【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【云原生】

2年前 (2022) 程序员胖胖胖虎阿
311 0 0

文章目录

  • 一、前言
  • 二、Seata简介
    • 三个角色
      • 案例中三个角色的交互
  • 三、SpringCloud 集成Seata(注册和配置均采用file方式)
    • 0、业务架构图
    • 1、MySQL数据库信息
      • 0)一键执行所有SQL
      • 1)undo_log 事务回滚日志表
        • undo_log表结构从哪里找?
      • 2)仓储服务(stock-service)业务表
      • 3)订单服务(order-service)业务表
      • 4)账户服务(account)业务表
      • 5)seata-server表结构
        • 1> global_table
        • 2> branch_table
        • 3> lock_table
        • seata-server相关表结构从哪里找?
    • 2、seata-server
      • 1)seata-server配置
        • 1> registry.conf
        • 2> file.conf
      • 2)启动seata-server
        • seata-server.sh脚本中的参数
    • 3、seata-client
    • 0、最上层父项目spring-cloud-center的pom.xml文件
      • 1)account-service
        • 1> pom.xml
        • 2> DataSourceConfig
        • 3> AccountController
        • 4> Account
        • 5> AccountDAO
        • 6> AccountService
        • 7> AccountApplication
        • 8> application.yml
        • 9> file.conf
        • 10> registry.conf
      • 2)order-service
        • 1> pom.xml
        • 2> DataSourceConfig
        • 3> OrderController
        • 4> Order
        • 5> AccountFeignClient
        • 6> OrderDAO
        • 7> OrderService
        • 8> OrderApplication
        • 9> application.yml
        • 10> file.conf 和 registry.conf
      • 3)stock-service
        • 1> pom.xml
        • 2> DataSourceConfig
        • 3> StockController
        • 4> Stock
        • 5> StockDAO
        • 6> StockService
        • 7> StockApplication
        • 8> application.yml
        • 9> file.conf 和 registry.conf
      • 4)trade-center
        • 1> pom.xml
        • 2> TradeController
        • 3> OrderFeignClient
        • 4> StockFeignClient
        • 5> TradeService
        • 6> TradeApplication
        • 7> application.yml
        • 8> file.conf 和 registry.conf
    • 4、AT模式分布式事务效果演示
      • 1)请求正常
      • 2)请求异常
  • 四、总结和后续

一、前言

至此,微服务系列正式开启分布式事务篇;

捎带一提,seata官方给的案例是真的******,版本之间的差异并未说明,据悉官方案例属于政治任务!在开启案例之前,博主和网友们踩过一些坑,具体见文章:

  1. can not get cluster name in registry config ‘service.vgroupMapping.xx‘, please make sure registry问题解决;
  2. Seata Failed to get available servers: endpoint format should like ip:port 报错原因/解决方案汇总版(看完本文必解决问题)
  3. Seata json decode exception, Cannot construct instance of java.time.LocalDateTime报错原因/解决方案最全汇总版

本文基于AT模式 + File配置/注册搭建SpringCloud 和 Seata的集成案例;

版本信息如下:

<properties>
    <spring-boot.version>2.4.2</spring-boot.version>
    <spring-cloud.version>2020.0.1</spring-cloud.version>
    <spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version>
    <mysql.version>8.0.22</mysql.version>
</properties>

二、Seata简介

【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【云原生】

Seata 是一款开源的分布式事务解决方案,全称:Simple extensiable autonomous transaction architecture;意思是:简单的、可扩展的、自治的事务架构。Seata致力于提供高性能和简单易用的分布式事务服务;Seata 为用户提供了 AT、TCC、SAGA 和 XA 四种分布式事务模式;

1> AT模式:

  • 提供无侵入自动补偿的事务模式,目前已支持MySQL、Oracle、PostgreSQL、TiDB 和 MariaDB;

2> TCC 模式:

  • 支持 TCC 模式并可与 AT 混用,灵活度更高;

3> SAGA 模式:

  • 为长事务提供有效的解决方案,提供编排式与注解式(开发中);

4> XA 模式:

  • 支持已实现 XA 接口的数据库的 XA 模式,目前已支持MySQL、Oracle、TiDB和MariaDB;

官方文档:https://seata.io/zh-cn/docs/overview/what-is-seata.html

三个角色

1> TC (Transaction Coordinator) - 事务协调者

  • 维护全局和分支事务的状态,驱动全局事务提交或回滚。

2> TM (Transaction Manager) - 事务管理器

  • 定义全局事务的范围:开始全局事务、提交或回滚全局事务。

3> RM (Resource Manager) - 资源管理器

  • 管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

案例中三个角色的交互

【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【云原生】

三、SpringCloud 集成Seata(注册和配置均采用file方式)

本文基于AT模式 + File配置/注册搭建SpringCloud 和 Seata的集成案例;

整体项目目录包括四个Module,分别为:trade-center、stock-service、order-service、account-service。
【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【云原生】

用例为用户购买商品的业务逻辑,整个业务逻辑由3个微服务提供支持,其中:

  • 仓储服务(stock-service):对给定的商品扣除仓储数量。
  • 订单服务(order-service):根据采购需求创建订单。
  • 帐户服务(account-service):从用户帐户中扣除余额。

此外,trade-center为交易中心,是处理用户请求的入口;

0、业务架构图

【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【云原生】

1、MySQL数据库信息

必须要使用具有InnoDB引擎的MySQL;也就是说数据库的引擎要支持事务,因为AT模式底层是依赖数据库事务实现的分布式事务。

在案例中,仓储服务(stock-service)、订单服务(order-service)、帐户服务(account-service) 这三个服务对应三个数据库,为了方便测试,我们只创建一个数据库并配置3个数据源。

0)一键执行所有SQL

1> 案例中seata-client相关的所有业务库、业务表、undo_log表创建SQL;
2> seata-server保存数据的表;

#Account
DROP SCHEMA IF EXISTS seata_account;
CREATE SCHEMA seata_account;
USE seata_account;

CREATE TABLE `account_tbl`
(
    `id`      INT(11) NOT NULL AUTO_INCREMENT,
    `user_id` VARCHAR(255) DEFAULT NULL,
    `money`   INT(11) DEFAULT 0,
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

INSERT INTO account_tbl (id, user_id, money)
VALUES (1, '1001', 10000);
INSERT INTO account_tbl (id, user_id, money)
VALUES (2, '1002', 10000);

CREATE TABLE `undo_log`
(
    `id`            bigint(20) NOT NULL AUTO_INCREMENT,
    `branch_id`     bigint(20) NOT NULL,
    `xid`           varchar(100) NOT NULL,
    `context`       varchar(128) NOT NULL,
    `rollback_info` longblob     NOT NULL,
    `log_status`    int(11) NOT NULL,
    `log_created`   datetime     NOT NULL,
    `log_modified`  datetime     NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

#Order
DROP SCHEMA IF EXISTS seata_order;
CREATE SCHEMA seata_order;
USE seata_order;

CREATE TABLE `order_tbl`
(
    `id`             INT(11) NOT NULL AUTO_INCREMENT,
    `user_id`        VARCHAR(255) DEFAULT NULL,
    `commodity_code` VARCHAR(255) DEFAULT NULL,
    `count`          INT(11) DEFAULT '0',
    `money`          INT(11) DEFAULT '0',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

CREATE TABLE `undo_log`
(
    `id`            bigint(20) NOT NULL AUTO_INCREMENT,
    `branch_id`     bigint(20) NOT NULL,
    `xid`           varchar(100) NOT NULL,
    `context`       varchar(128) NOT NULL,
    `rollback_info` longblob     NOT NULL,
    `log_status`    int(11) NOT NULL,
    `log_created`   datetime     NOT NULL,
    `log_modified`  datetime     NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

#Stock
DROP SCHEMA IF EXISTS seata_stock;
CREATE SCHEMA seata_stock;
USE seata_stock;

CREATE TABLE `stock_tbl`
(
    `id`             INT(11) NOT NULL AUTO_INCREMENT,
    `commodity_code` VARCHAR(255) DEFAULT NULL,
    `count`          INT(11) DEFAULT '0',
    PRIMARY KEY (`id`),
    UNIQUE KEY `commodity_code` (`commodity_code`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;


INSERT INTO stock_tbl (id, commodity_code, count)
VALUES (1, '2001', 1000);

CREATE TABLE `undo_log`
(
    `id`            bigint(20) NOT NULL AUTO_INCREMENT,
    `branch_id`     bigint(20) NOT NULL,
    `xid`           varchar(100) NOT NULL,
    `context`       varchar(128) NOT NULL,
    `rollback_info` longblob     NOT NULL,
    `log_status`    int(11) NOT NULL,
    `log_created`   datetime     NOT NULL,
    `log_modified`  datetime     NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

-- the table to store GlobalSession data
DROP SCHEMA IF EXISTS seata_server;
CREATE SCHEMA seata_server;
USE seata_server;

CREATE TABLE IF NOT EXISTS `global_table`
(
    `xid`                       VARCHAR(128) NOT NULL,
    `transaction_id`            BIGINT,
    `status`                    TINYINT      NOT NULL,
    `application_id`            VARCHAR(32),
    `transaction_service_group` VARCHAR(32),
    `transaction_name`          VARCHAR(128),
    `timeout`                   INT,
    `begin_time`                BIGINT,
    `application_data`          VARCHAR(2000),
    `gmt_create`                DATETIME,
    `gmt_modified`              DATETIME,
    PRIMARY KEY (`xid`),
    KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
    KEY `idx_transaction_id` (`transaction_id`)
    ) ENGINE = InnoDB
    DEFAULT CHARSET = utf8;

-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
    `branch_id`         BIGINT       NOT NULL,
    `xid`               VARCHAR(128) NOT NULL,
    `transaction_id`    BIGINT,
    `resource_group_id` VARCHAR(32),
    `resource_id`       VARCHAR(256),
    `branch_type`       VARCHAR(8),
    `status`            TINYINT,
    `client_id`         VARCHAR(64),
    `application_data`  VARCHAR(2000),
    `gmt_create`        DATETIME(6),
    `gmt_modified`      DATETIME(6),
    PRIMARY KEY (`branch_id`),
    KEY `idx_xid` (`xid`)
    ) ENGINE = InnoDB
    DEFAULT CHARSET = utf8;

-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
    `row_key`        VARCHAR(128) NOT NULL,
    `xid`            VARCHAR(128),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_branch_id` (`branch_id`)
    ) ENGINE = InnoDB
    DEFAULT CHARSET = utf8;

1)undo_log 事务回滚日志表

SEATA AT 模式需要 undo_log 表,用于事务回滚使用。所以上面三个服务每个服务都要有一个undo_log表。表结构如下:
【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【云原生】
建表SQL:

CREATE TABLE `undo_log`
(
    `id`            bigint(20) NOT NULL AUTO_INCREMENT,
    `branch_id`     bigint(20) NOT NULL,
    `xid`           varchar(100) NOT NULL,
    `context`       varchar(128) NOT NULL,
    `rollback_info` longblob     NOT NULL,
    `log_status`    int(11) NOT NULL,
    `log_created`   datetime     NOT NULL,
    `log_modified`  datetime     NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

**undo_log表的结构是从哪里找的?为什么它是这个?**看了一些文章并没有说这个,本文简要说明一下;

undo_log表结构从哪里找?

1> 在GitHub中找到seata的源码,选择响应版本的代码分支:

  • 源码地址:https://github.com/seata/seata/tree/1.3.0

【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【云原生】

注意源码最上层目录结构下有一个script文件夹,其中记录了所有我们可能需要的SQL、配置…。比如:集成Nacos时,配置的内容、上传配置的shell脚本都在其中。

2> 进入目录/script/client/at/db,找到mysql.sql文件,其就是我们需要的创建undo_log表结构的SQL:

【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【云原生】

2)仓储服务(stock-service)业务表

1> 表结构:

【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【云原生】

2> 建表SQL:

DROP SCHEMA IF EXISTS seata_stock;
CREATE SCHEMA seata_stock;
USE seata_stock;

CREATE TABLE `stock_tbl`
(
    `id`             INT(11) NOT NULL AUTO_INCREMENT,
    `commodity_code` VARCHAR(255) DEFAULT NULL,
    `count`          INT(11) DEFAULT '0',
    PRIMARY KEY (`id`),
    UNIQUE KEY `commodity_code` (`commodity_code`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

INSERT INTO stock_tbl (id, commodity_code, count)
VALUES (1, '2001', 1000);

3)订单服务(order-service)业务表

1> 表结构:

【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【云原生】

2> 建表SQL:

DROP SCHEMA IF EXISTS seata_order;
CREATE SCHEMA seata_order;
USE seata_order;

CREATE TABLE `order_tbl`
(
    `id`             INT(11) NOT NULL AUTO_INCREMENT,
    `user_id`        VARCHAR(255) DEFAULT NULL,
    `commodity_code` VARCHAR(255) DEFAULT NULL,
    `count`          INT(11) DEFAULT '0',
    `money`          INT(11) DEFAULT '0',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

4)账户服务(account)业务表

1> 表结构:

【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【云原生】

2> 建表SQL:

DROP SCHEMA IF EXISTS seata_account;
CREATE SCHEMA seata_account;
USE seata_account;

CREATE TABLE `account_tbl`
(
    `id`      INT(11) NOT NULL AUTO_INCREMENT,
    `user_id` VARCHAR(255) DEFAULT NULL,
    `money`   INT(11) DEFAULT 0,
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

INSERT INTO account_tbl (id, user_id, money)
VALUES (1, '1001', 10000);
INSERT INTO account_tbl (id, user_id, money)
VALUES (2, '1002', 10000);

5)seata-server表结构

当seata-server配置信息中 store配置的是db时,
【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【云原生】

需要使用到三张表:global_table(记录全局事务)、branch_table(记录分支事务)、lock_table(记录全局锁);

当然数据库表和表名是可以改变的,只需要在store配置中对应上即可。

1> global_table

1> 表结构:

【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【云原生】

2> 建表SQL:

CREATE TABLE IF NOT EXISTS `global_table`
(
    `xid`                       VARCHAR(128) NOT NULL,
    `transaction_id`            BIGINT,
    `status`                    TINYINT      NOT NULL,
    `application_id`            VARCHAR(32),
    `transaction_service_group` VARCHAR(32),
    `transaction_name`          VARCHAR(128),
    `timeout`                   INT,
    `begin_time`                BIGINT,
    `application_data`          VARCHAR(2000),
    `gmt_create`                DATETIME,
    `gmt_modified`              DATETIME,
    PRIMARY KEY (`xid`),
    KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
    KEY `idx_transaction_id` (`transaction_id`)
    ) ENGINE = InnoDB
    DEFAULT CHARSET = utf8;

2> branch_table

1> 表结构:

【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【云原生】

2> 建表SQL:

CREATE TABLE IF NOT EXISTS `branch_table`
(
    `branch_id`         BIGINT       NOT NULL,
    `xid`               VARCHAR(128) NOT NULL,
    `transaction_id`    BIGINT,
    `resource_group_id` VARCHAR(32),
    `resource_id`       VARCHAR(256),
    `branch_type`       VARCHAR(8),
    `status`            TINYINT,
    `client_id`         VARCHAR(64),
    `application_data`  VARCHAR(2000),
    `gmt_create`        DATETIME(6),
    `gmt_modified`      DATETIME(6),
    PRIMARY KEY (`branch_id`),
    KEY `idx_xid` (`xid`)
    ) ENGINE = InnoDB
    DEFAULT CHARSET = utf8;

3> lock_table

1> 表结构:

【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【云原生】

2> 建表SQL:

CREATE TABLE IF NOT EXISTS `lock_table`
(
    `row_key`        VARCHAR(128) NOT NULL,
    `xid`            VARCHAR(128),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_branch_id` (`branch_id`)
    ) ENGINE = InnoDB
    DEFAULT CHARSET = utf8;

seata-server相关表结构从哪里找?

1> 在GitHub中找到seata的源码,选择响应版本的代码分支:

  • 源码地址:https://github.com/seata/seata/tree/1.3.0

【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【云原生】

注意源码最上层目录结构下有一个script文件夹,其中记录了所有我们可能需要的SQL、配置…。比如:集成Nacos时,配置的内容、上传配置的shell脚本都在其中。

2> 进入目录/script/server/db,找到mysql.sql文件,其就是我们需要的创建seata-server相关的表结构的SQL:

【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【云原生】

数据库表结构处理完之后,看一下seata-server需要如何下载、配置、启动?

2、seata-server

seata-server下载地址:https://seata.io/zh-cn/blog/download.html,其中binary选项为seata-server可执行程序,source为相应版本源码。

1)seata-server配置

将下载下来的seata-server-1.3.0.tar.gz压缩包解压,解压后的文件目录为:seata-server-1.3.0

# 进入seata-server主目录
cd seata-server-1.3.0
# 进入seata-server配置目录
cd conf

修改registry.conffile.conf配置文件,内容如下:

1> registry.conf

seata-server的配置中心和注册中心均采用file的方式:

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "file"

  nacos {
    application = "seata-server"
    serverAddr = "127.0.0.1:8848"
    group = "SEATA_GROUP"
    namespace = ""
    cluster = "default"
    username = ""
    password = ""
  }
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    application = "default"
    weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = 0
    password = ""
    cluster = "default"
    timeout = 0
  }
  zk {
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  consul {
    cluster = "default"
    serverAddr = "127.0.0.1:8500"
  }
  etcd3 {
    cluster = "default"
    serverAddr = "http://localhost:2379"
  }
  sofa {
    serverAddr = "127.0.0.1:9603"
    application = "default"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    cluster = "default"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
  }
  file {
    name = "file.conf"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "file"

  nacos {
    serverAddr = "127.0.0.1:8848"
    namespace = ""
    group = "SEATA_GROUP"
    username = ""
    password = ""
  }
  consul {
    serverAddr = "127.0.0.1:8500"
  }
  apollo {
    appId = "seata-server"
    apolloMeta = "http://192.168.1.204:8801"
    namespace = "application"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  file {
    name = "file.conf"
  }
}

2> file.conf

seata-server的配置中心采用file时,具体配置如下:


## transaction log store, only used in seata-server
store {
  ## store mode: file、db、redis
  mode = "db"

  ## database store property
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
    datasource = "druid"
    ## mysql/oracle/postgresql/h2/oceanbase etc.
    dbType = "mysql"
    driverClassName = "com.mysql.cj.jdbc.Driver"
    url = "jdbc:mysql://127.0.0.1:3306/seata_server"
    user = "root"
    password = "123456"
    minConn = 5
    maxConn = 30
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
    maxWait = 5000
  }

}

transport {
  # tcp udt unix-domain-socket
  type = "TCP"
  #NIO NATIVE
  server = "NIO"
  #enable heartbeat
  heartbeat = true
  # the client batch send request enable
  enableClientBatchSendRequest = false
  #thread factory for netty
  threadFactory {
    bossThreadPrefix = "NettyBoss"
    workerThreadPrefix = "NettyServerNIOWorker"
    serverExecutorThreadPrefix = "NettyServerBizHandler"
    shareBossWorker = false
    clientSelectorThreadPrefix = "NettyClientSelector"
    clientSelectorThreadSize = 1
    clientWorkerThreadPrefix = "NettyClientWorkerThread"
    # netty boss thread size,will not be used for UDT
    bossThreadSize = 1
    #auto default pin or 8
    workerThreadSize = "default"
  }
  shutdown {
    # when destroy server, wait seconds
    wait = 3
  }
  serialization = "seata"
  compressor = "none"
}


transaction {
  undo.data.validation = true
  undo.log.serialization = "jackson"
}

## metrics configuration, only used in server side
metrics {
  enabled = false
  registryType = "compact"
  # multi exporters use comma divided
  exporterList = "prometheus"
  exporterPrometheusPort = 9898
}

2)启动seata-server

进入seata-server-1.3.0/bin目录,然后运行seata-server.sh shell脚本;

cd ../bin
sh seata-server.sh

seata-server.sh脚本中的参数

官方文档介绍:https://github.com/seata/seata/tree/develop/script/config-center;

采用File配置方式时不需要关注,当使用其他配置中心时再关注即可(留坑集成Nacos时处理)。

3、seata-client

一般Spring Cloud集成seata大致会分为5步:

1> 第一步:

  • 添加Spring Cloud Alibaba 依赖管理工具和 Seata 依赖;
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2021.1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    
    <dependency>
    	<groupId>com.alibaba.cloud</groupId>
    	<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    </dependency>
    

另外,spring-cloud-starter-alibaba-seata依赖中seata相关的只依赖了spring-cloud-alibaba-seata,所以在项目中添加spring-cloud-starter-alibaba-seataspring-cloud-alibaba-seata是一样的;

2> 第二步:

  • 添加Seata配置文件,包括:registry.conf、file.conf(如果使用file作为配置中心),其中:
  • registry.conf用于指定 TC 的注册中心和配置文件,默认都是 file; 如果使用其他的注册中心,要求 Seata-Server 也注册到该配置中心上;
  • file.conf用于指定TC的相关属性;如果使用注册中心也可以将配置添加到配置中心;

3> 第三步:

  • 注入数据源;Seata 通过代理数据源的方式实现分支事务;MyBatis 和 JPA 都需要注入 io.seata.rm.datasource.DataSourceProxy, 不同的是,MyBatis 还需要额外注入 org.apache.ibatis.session.SqlSessionFactory

4> 第四步:

  • 在业务相关的数据库中添加 undo_log 表,用于保存需要回滚的数据;

5> 第五步:

  • 在业务的发起方的方法上使用@GlobalTransactional开启全局事务,Seata 会将事务的 xid 通过拦截器添加到调用其他服务的请求中,实现分布式事务;

上面提到整体项目目录包括四个Module,分别为:trade-center、stock-service、order-service、account-service。
【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【云原生】

下面从这四个Module以具体的代码来看,这五步是如何体现在代码中的;

0、最上层父项目spring-cloud-center的pom.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <modules>
        <module>trade-center</module>
        <module>stock-service</module>
        <module>order-service</module>
        <module>account-service</module>
    </modules>

    <groupId>com.saint</groupId>
    <artifactId>transaction-seata</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>transaction-seata</name>
    <description>transaction-seata</description>
    <packaging>pom</packaging>

    <properties>
        <java.version>1.8</java.version>
        <spring-boot.version>2.4.2</spring-boot.version>
        <spring-cloud.version>2020.0.1</spring-cloud.version>
        <spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version>
        <druid.version>1.2.8</druid.version>
        <mysql.version>8.0.22</mysql.version>
    </properties>


    <dependencies>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.12.0</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.10</version>
        </dependency>
    </dependencies>

    <dependencyManagement>

        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--整合spring cloud-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--整合spring cloud alibaba-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                <version>${druid.version}</version>
            </dependency>

            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

关于Spring-cloud和SpringBoot的版本对应关系,参考博文:SpringBoot、SpringCloud、SpringCloudAlibaba的版本对应关系。

1)account-service

account-service整体代码结构目录如下:
【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【云原生】

整体包括:pom.xml、JPA数据源配置、一个Controller、一个entity、一个Dao、一个Service、一个启动类;resources目录下三个配置文件:application.yml、file.conf、registry.conf;

1> pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>transaction-seata</artifactId>
        <groupId>com.saint</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <version>0.0.1-SNAPSHOT</version>
    <groupId>com.saint</groupId>
    <artifactId>account-service</artifactId>
    <name>account-service</name>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>
    </dependencies>

</project>

2> DataSourceConfig

package com.saint.account.config;

import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;

/**
 * 数据源配置
 *
 * @author Saint
 */
@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DruidDataSource druidDataSource() {
        return new DruidDataSource();
    }

    /**
     * 需要将 DataSourceProxy 设置为主数据源,否则事务无法回滚
     *
     * @param druidDataSource The DruidDataSource
     * @return The default datasource
     */
    @Primary
    @Bean("dataSource")
    public DataSource dataSource(DruidDataSource druidDataSource) {
        return new DataSourceProxy(druidDataSource);
    }
}

3> AccountController

package com.saint.account.controller;

import com.saint.account.service.AccountService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;

/**
 * @author Saint
 */
@RestController
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
public class AccountController {

    private final AccountService accountService;

    @RequestMapping("/debit")
    public Boolean debit(String userId, BigDecimal money) {
        accountService.debit(userId, money);

        return true;
    }
}

4> Account

package com.saint.account.entity;

import lombok.Data;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.math.BigDecimal;

/**
 * @author Saint
 */
@Entity
@Table(name = "account_tbl")
@DynamicUpdate
@DynamicInsert
@Data
public class Account {
    @Id
    private Long id;
    private String userId;
    private BigDecimal money;
}

5> AccountDAO

package com.saint.account.repository;

import com.saint.account.entity.Account;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * @author Saint
 */
public interface AccountDAO extends JpaRepository<Account, Long> {

    Account findByUserId(String userId);

}

6> AccountService

package com.saint.account.service;

import com.saint.account.entity.Account;
import com.saint.account.repository.AccountDAO;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;

/**
 * @author Saint
 */
@Service
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
public class AccountService {

    private final AccountDAO accountDAO;

    private static final String ERROR_USER_ID = "1002";

    @Transactional(rollbackFor = Exception.class)
    public void debit(String userId, BigDecimal num) {
        Account account = accountDAO.findByUserId(userId);
        account.setMoney(account.getMoney().subtract(num));
        accountDAO.save(account);

        if (ERROR_USER_ID.equals(userId)) {
            throw new RuntimeException("account branch exception");
        }
    }
}

7> AccountApplication

package com.saint.account;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

/**
 * @author Saint
 */
@SpringBootApplication
@EnableFeignClients
@EnableJpaRepositories
public class AccountApplication {

    public static void main(String[] args) {
        SpringApplication.run(AccountApplication.class, args);
    }
}

8> application.yml

server:
  port: 9031
spring:
  application:
    name: account-service
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/seata_account?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    show-sql: true

seata:
  # 属性需要seata-server端 config.txt文件中的service.vgroupMapping后的值保持一致
  # 即,nacos配置中心中要存在dataId为 service.vgroupMapping.saint-trade-tx-group 的配置
  tx-service-group: saint-trade-tx-group

9> file.conf

transport {
  # tcp udt unix-domain-socket
  type = "TCP"
  #NIO NATIVE
  server = "NIO"
  #enable heartbeat
  heartbeat = true
  # the client batch send request enable
  enableClientBatchSendRequest = true
  #thread factory for netty
  threadFactory {
    bossThreadPrefix = "NettyBoss"
    workerThreadPrefix = "NettyServerNIOWorker"
    serverExecutorThread-prefix = "NettyServerBizHandler"
    shareBossWorker = false
    clientSelectorThreadPrefix = "NettyClientSelector"
    clientSelectorThreadSize = 1
    clientWorkerThreadPrefix = "NettyClientWorkerThread"
    # netty boss thread size,will not be used for UDT
    bossThreadSize = 1
    #auto default pin or 8
    workerThreadSize = "default"
  }
  shutdown {
    # when destroy server, wait seconds
    wait = 3
  }
  serialization = "seata"
  compressor = "none"
}
service {
  #transaction service group mapping
  vgroupMapping.saint-trade-tx-group = "seata-server-sh"
  #only support when registry.type=file, please don't set multiple addresses
  seata-server-sh.grouplist = "127.0.0.1:8091"
  #degrade, current not support
  enableDegrade = false
  #disable seata
  disableGlobalTransaction = false
}

client {
  rm {
    asyncCommitBufferLimit = 10000
    lock {
      retryInterval = 10
      retryTimes = 30
      retryPolicyBranchRollbackOnConflict = true
    }
    reportRetryCount = 5
    tableMetaCheckEnable = false
    reportSuccessEnable = false
  }
  tm {
    commitRetryCount = 5
    rollbackRetryCount = 5
  }
  undo {
    dataValidation = true
    logSerialization = "jackson"
    logTable = "undo_log"
  }
  log {
    exceptionRate = 100
  }
}

10> registry.conf

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "file"

  nacos {
    application = "seata-server"
    serverAddr = "127.0.0.1"
    namespace = ""
    username = ""
    password = ""
  }
  file {
    name = "file.conf"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3、springCloudConfig
  type = "file"

  nacos {
    serverAddr = "127.0.0.1"
    namespace = ""
    group = "SEATA_GROUP"
    username = ""
    password = ""
  }
  file {
    name = "file.conf"
  }
}

2)order-service

order-service整体代码结构目录如下:
【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【云原生】

整体包括:pom.xml、JPA数据源配置、一个Controller、一个entity、一个FeignClient、一个Dao、一个Service、一个启动类;resources目录下三个配置文件:application.yml、file.conf、registry.conf;

1> pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>transaction-seata</artifactId>
        <groupId>com.saint</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <version>0.0.1-SNAPSHOT</version>
    <groupId>com.saint</groupId>
    <artifactId>order-service</artifactId>
    <name>order-service</name>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>

    </dependencies>

</project>

2> DataSourceConfig

JPA的数据源配置和account-service中的一样;

3> OrderController

package com.saint.order.controller;

import com.saint.order.service.OrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author Saint
 */
@RestController
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
public class OrderController {

    private final OrderService orderService;

    @GetMapping("/create")
    public Boolean create(String userId, String commodityCode, Integer count) {

        orderService.create(userId, commodityCode, count);
        return true;
    }

}

4> Order

package com.saint.order.entity;

import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;

import javax.persistence.*;
import java.math.BigDecimal;

/**
 * @author Saint
 */
@Entity
@Table(name = "order_tbl")
@DynamicUpdate
@DynamicInsert
@NoArgsConstructor
@Data
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "user_id")
    private String userId;

    @Column(name = "commodity_code")
    private String commodityCode;

    @Column(name = "money")
    private BigDecimal money;

    @Column(name = "count")
    private Integer count;

}

5> AccountFeignClient

AccountFeignClient用于通过OpenFeign调用account-service;

package com.saint.order.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.math.BigDecimal;

/**
 * @author Saint
 */
@FeignClient(name = "account-service", url = "127.0.0.1:9031")
public interface AccountFeignClient {

    @GetMapping("/debit")
    Boolean debit(@RequestParam("userId") String userId, @RequestParam("money") BigDecimal money);
}

6> OrderDAO

package com.saint.order.repository;

import com.saint.order.entity.Order;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * @author Saint
 */
public interface OrderDAO extends JpaRepository<Order, Long> {

}

7> OrderService

package com.saint.order.service;

import com.saint.order.entity.Order;
import com.saint.order.feign.AccountFeignClient;
import com.saint.order.repository.OrderDAO;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;

/**
 * @author Saint
 */
@Service
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
public class OrderService {

    private final AccountFeignClient accountFeignClient;
    private final OrderDAO orderDAO;

    @Transactional
    public void create(String userId, String commodityCode, Integer count) {

        BigDecimal orderMoney = new BigDecimal(count).multiply(new BigDecimal(5));
        Order order = new Order();
        order.setUserId(userId);
        order.setCommodityCode(commodityCode);
        order.setCount(count);
        order.setMoney(orderMoney);

        orderDAO.save(order);
        accountFeignClient.debit(userId, orderMoney);

    }

}

8> OrderApplication

package com.saint.order;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

/**
 * @author Saint
 */
@SpringBootApplication
@EnableFeignClients
@EnableJpaRepositories
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }

}

9> application.yml

server:
  port: 9021
spring:
  application:
    name: order-service
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/seata_order?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    show-sql: true

seata:
  # 属性需要seata-server端 config.txt文件中的service.vgroupMapping后的值保持一致
  # 即,nacos配置中心中要存在dataId为 service.vgroupMapping.saint-trade-tx-group 的配置
  tx-service-group: saint-trade-tx-group

10> file.conf 和 registry.conf

file.conf 和 registry.conf与account-service的一样;

3)stock-service

stck-service整体代码结构目录如下:
【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【云原生】

整体包括:pom.xml、JPA数据源配置、一个Controller、一个entity、一个Dao、一个Service、一个启动类;resources目录下三个配置文件:application.yml、file.conf、registry.conf;

1> pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>transaction-seata</artifactId>
        <groupId>com.saint</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <version>0.0.1-SNAPSHOT</version>
    <groupId>com.saint</groupId>
    <artifactId>stock-service</artifactId>
    <name>stock-service</name>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>
    </dependencies>

</project>

2> DataSourceConfig

JPA的数据源配置和account-service中的一样;

3> StockController

package com.saint.stock.controller;

import com.saint.stock.service.StockService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author Saint
 */
@RestController
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
public class StockController {

    private final StockService stockService;

    @GetMapping(path = "/deduct")
    public Boolean deduct(String commodityCode, Integer count) {
        stockService.deduct(commodityCode, count);
        return true;
    }
}

4> Stock

package com.saint.stock.entity;

import lombok.Data;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

/**
 * @author Saint
 */
@Entity
@Table(name = "stock_tbl")
@DynamicUpdate
@DynamicInsert
@Data
public class Stock {

    @Id
    private Long id;
    private String commodityCode;
    private Integer count;
}

5> StockDAO

package com.saint.stock.repository;

import com.saint.stock.entity.Stock;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * @author Saint
 */
public interface StockDAO extends JpaRepository<Stock, String> {

    Stock findByCommodityCode(String commodityCode);

}

6> StockService

package com.saint.stock.service;

import com.saint.stock.entity.Stock;
import com.saint.stock.repository.StockDAO;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author Saint
 */
@Service
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
public class StockService {

    private final StockDAO stockDAO;

    @Transactional
    public void deduct(String commodityCode, int count) {
        Stock stock = stockDAO.findByCommodityCode(commodityCode);
        stock.setCount(stock.getCount() - count);

        stockDAO.save(stock);
    }
}

7> StockApplication

package com.saint.stock;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

/**
 * @author Saint
 */
@SpringBootApplication
@EnableJpaRepositories
public class StockApplication {
    public static void main(String[] args) {
        SpringApplication.run(StockApplication.class, args);
    }
}

8> application.yml

server:
  port: 9011
spring:
  application:
    name: stock-service
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/seata_stock?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    show-sql: true

seata:
  # 属性需要seata-server端 config.txt文件中的service.vgroupMapping后的值保持一致
  # 即,nacos配置中心中要存在dataId为 service.vgroupMapping.saint-trade-tx-group 的配置
  tx-service-group: saint-trade-tx-group

9> file.conf 和 registry.conf

file.conf 和 registry.conf和account-service一样;

4)trade-center

trade-center整体代码结构目录如下:
【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【云原生】

整体包括:pom.xml、一个Controller、一个entity、两个FeignClient、一个Service、一个启动类;resources目录下三个配置文件:application.yml、file.conf、registry.conf;

1> pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>transaction-seata</artifactId>
        <groupId>com.saint</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>trade-center</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <groupId>com.saint</groupId>
    <name>trade-center</name>

    <dependencies>

        <!--openFeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <!--seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

    </dependencies>

</project>

2> TradeController

package com.saint.trade.controller;

import com.saint.trade.service.TradeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author Saint
 */
@RestController
public class TradeController {

    @Autowired
    private TradeService businessService;

    /**
     * 购买下单,模拟全局事务提交
     *
     * @return
     */
    @RequestMapping("/purchase/commit")
    public Boolean purchaseCommit() {
        businessService.purchase("1001", "2001", 1);
        return true;
    }

    /**
     * 购买下单,模拟全局事务回滚
     *
     * @return
     */
    @RequestMapping("/purchase/rollback")
    public Boolean purchaseRollback() {
        try {
            businessService.purchase("1002", "2001", 1);
//            businessService.failToPurchase("1001", "2001", 1);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

        return true;
    }
}

3> OrderFeignClient

trade-center通过OpenFeign调用order-service;

package com.saint.trade.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

/**
 * @author Saint
 */
@FeignClient(name = "order-service", url = "127.0.0.1:9021")
public interface OrderFeignClient {

    @GetMapping("/create")
    void create(@RequestParam("userId") String userId, @RequestParam("commodityCode") String commodityCode,
                @RequestParam("count") Integer count);

}

4> StockFeignClient

trade-center通过OpenFeign调用stock-service;

package com.saint.trade.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

/**
 * @author Saint
 */
@FeignClient(name = "stock-service", url = "127.0.0.1:9011")
public interface StockFeignClient {

    @GetMapping("/deduct")
    void deduct(@RequestParam("commodityCode") String commodityCode, @RequestParam("count") Integer count);

}

5> TradeService

TradeService中通过@GlobalTransactional开启分布式事务;

package com.saint.trade.service;

import com.saint.trade.feign.OrderFeignClient;
import com.saint.trade.feign.StockFeignClient;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

/**
 * @author Saint
 */
@Service
@RequiredArgsConstructor
public class TradeService {

    private final StockFeignClient stockFeignClient;
    private final OrderFeignClient orderFeignClient;

    /**
     * 减库存,下订单
     *
     * @param userId
     * @param commodityCode
     * @param orderCount
     */
    @GlobalTransactional
    public void purchase(String userId, String commodityCode, int orderCount) {
        stockFeignClient.deduct(commodityCode, orderCount);

        orderFeignClient.create(userId, commodityCode, orderCount);
    }

    /**
     * 减库存,下订单(有异常)
     *
     * @param userId
     * @param commodityCode
     * @param orderCount
     */
    @GlobalTransactional
    public void failToPurchase(String userId, String commodityCode, int orderCount) {
        stockFeignClient.deduct(commodityCode, orderCount);
        orderFeignClient.create(userId, commodityCode, orderCount);
        throw new RuntimeException("Error!");
    }
}

6> TradeApplication

package com.saint.trade;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * @author Saint
 */
@SpringBootApplication
@EnableFeignClients
public class TradeApplication {

    public static void main(String[] args) {
        SpringApplication.run(TradeApplication.class, args);
    }
}

7> application.yml

server:
  port: 9001
spring:
  application:
    name: trade-center

seata:
  # 属性需要seata-server端 config.txt文件中的service.vgroupMapping后的值保持一致
  # 即,nacos配置中心中要存在dataId为 service.vgroupMapping.saint-trade-tx-group 的配置
  tx-service-group: saint-trade-tx-group

8> file.conf 和 registry.conf

file.conf 和 registry.conf 与 account-service的一样;

4、AT模式分布式事务效果演示

分别启动trade-center、stock-service、order-service、account-service;

1)请求正常

分布式事务成功,模拟正常下单、扣库存

请求访问:http://127.0.0.1:9001/purchase/commit

2)请求异常

分布式事务失败,模拟下单成功、扣库存失败,最终同时回滚

请求访问:http://127.0.0.1:9001/purchase/rollback;

四、总结和后续

当前文章讲述了Spring Cloud + JPA + OpenFeign + Seata实现分布式事务的案例。

下一篇文章为:Spring Cloud 整合Seata + Nacos。

相关文章

暂无评论

暂无评论...