Spring Cloud Alibaba 分布式事务 Seata 入门

文章目录

        • 1. Seata 简介
          • 1.1 四种事务模式
          • 1.2 三个基本组件
          • 1.3 seata分布式事务的生命周期
        • 2. Seata TC Server 安装
          • 2.1 单机部署
          • 2.2 集群部署
        • 3. Seata AT 模式整合OpenFeign
          • 3.1 初始化数据库
          • 3.2 搭建订单服务
          • 3.3 搭建仓储服务
          • 3.4 搭建账户服务
        • 4. 启动服务,开始测试
          • 4.1 分布式事务正常提交测试
          • 4.2 分布式事务异常回滚测试
        • 5. 示例完整代码下载

1. Seata 简介

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。

seata官网地址

1.1 四种事务模式
Seata 将为用户提供 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
  • AT 模式:提供无侵入自动补偿的事务模式,详细可参考《Seata AT 模式》官方文档
  • TCC 模式:支持调用自定义的提交或者回滚的逻辑,详细可参考《Seata TCC 模式》官方文档
  • Saga 模式:为长事务提供有效的解决方案,详细可参考《Seata Saga 模式》官方文档
  • XA 模式:强一致性事务,拥有全局事务的隔离性,详细可参考《Seata XA 模式》官方文档
1.2 三个基本组件
seata由三个基本组件构成,如下图所示:

Spring Cloud Alibaba 分布式事务 Seata 入门_第1张图片

  • TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。
  • TM (Transaction Manager) - 事务管理器:定义全局事务的范围,开始全局事务、提交或回滚全局事务。
  • RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
1.3 seata分布式事务的生命周期

Spring Cloud Alibaba 分布式事务 Seata 入门_第2张图片
1、TM请求TC开始一个新的全局事务,TC生成一个表示全局事务的XID。
2、XID通过微服务的调用链传播。
3、RM将本地事务注册为XID对应全局事务的分支到TC。
4、TM请求TC提交或回滚XID对应的全局事务。
5、TC驱动XID对应全局事务下的所有分支事务来完成分支提交或回滚。

2. Seata TC Server 安装

tc需要存储全局事务和分支事务的数据,目前有两种存储模式:
  • file模式:适合单机部署,全局事务和分支事务的数据持久化到本地文件。

  • db模式:适合集群部署,全局事务和分支事务的数据通过db进行共享。

2.1 单机部署

打开 seata下载页面,选择最新的版本下载

# 创建seata目录
$ mkdir -p /usr/seata
$ cd /usr/seata

# 下载
$ wget https://www.github.com//seata/seata/releases/download/v1.4.0/seata-server-1.4.0.tar.gz
# 解压文件
$ tar -zxvf seata-server-1.4.0.tar.gz

# 查看seata目录
$ cd seata
$ ll
总用量 48
drwxr-xr-x. 2  root root     53 1月  19 10:21 bin # 启动TC的shell脚本
drwxr-xr-x. 3  root root   141 11月  2 17:30 conf # 配置文件
drwxr-xr-x. 3  root root  8192 11月  2 17:30 lib # 依赖的jar包
-rw-r--r--. 1  root root 11365 5月  13 2019 LICENSE

# 后台启动 tc server 
$ nohup sh bin/seata-server.sh & 

# 查看nohup.out日志,出现Server started表示启动成功,默认端口是8091
$ tail -f nohup.out
1:21:52.477  INFO --- [                     main] io.seata.config.FileConfiguration        : The file name of the operation is registry
11:21:52.486  INFO --- [                     main] io.seata.config.FileConfiguration        : The configuration file used is /usr/seata/seata/conf/registry.conf
11:21:53.094  INFO --- [                     main] io.seata.config.FileConfiguration        : The file name of the operation is file.conf
11:21:53.094  INFO --- [                     main] io.seata.config.FileConfiguration        : The configuration file used is file.conf
11:22:05.452  INFO --- [                     main] i.s.core.rpc.netty.NettyServerBootstrap  : Server started, listen port: 8091

# 再次查看seata目录,发现多了nohup.out文件和sessionStore目录
$ ll
drwxr-xr-x. 2 root root     53 1月  19 10:21 bin
drwxr-xr-x. 3  root root   141 11月  2 17:30 conf
drwxr-xr-x. 3  root root  8192 11月  2 17:30 lib
-rw-r--r--. 1  root root 11365 5月  13 2019 LICENSE
-rw-------. 1 root root  32966 1月  19 11:22 nohup.out
drwxr-xr-x. 2 root root     23 1月  19 10:26 sessionStore

# 查看sessionStore目录,file模式下持久化的文件就是 root.data
$ cd sessionStore
$ ll
-rw-r--r--. 1 root root 0 1月  19 10:26 root.data
2.2 集群部署
多个seata tc server实例组成了tc集群,它们通过db实现了全局和分支事务数据共享,则拥有了高可用的能力。每一个tc server都可以注册自己到注册中心,方便微服务应用通过注册中心获取它们的连接地址。整体架构图如下所示:

Spring Cloud Alibaba 分布式事务 Seata 入门_第3张图片

1、使用 mysql.sql 脚本初始化TC Server 数据库,脚本内容如下:

-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
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(96),
    `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;
2、创建seata数据库,执行上述sql脚本,完成后的结果如下图所示:

Spring Cloud Alibaba 分布式事务 Seata 入门_第4张图片

3、修改 conf/file.conf 配置文件,设置为db模式,内容如下所示:
## 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://192.168.0.118:3306/seata?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8"
    user = "root"
    password = "123456"
    minConn = 5
    maxConn = 100
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
    maxWait = 5000
  }
}
service {
     
  vgroupMapping.my_test_tx_group = "seata-tc-server"
}
4、修改 conf/registry.conf 配置文件,设置使用Eureka注册中心,application表示TC Server 注册到eureka显示的服务名称。
registry {
     
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "eureka"
  loadBalance = "RandomLoadBalance"
  loadBalanceVirtualNodes = 10

  eureka {
     
    serviceUrl = "http://192.168.0.118:8761/eureka"
    application = "seata-tc-server"
    weight = "1"
  }
}

5、指定端口号、节点编号启动TC Server,先后执行如下shell命令启动两个节点
# 启动第一个tc server节点
$ nohup sh bin/seata-server.sh -p 18090 -n 1 &

# 启动第二个tc server节点
$ nohup sh bin/seata-server.sh -p 28090 -n 2 &
打印 nohup.out 文件里面的日志
$ tail -f nohup.out 
14:23:49.581  INFO --- [                     main] io.seata.config.FileConfiguration        : The file name of the operation is registry
14:23:49.623  INFO --- [                     main] io.seata.config.FileConfiguration        : The configuration file used is /usr/seata/seata/conf/registry.conf
14:23:52.019  INFO --- [                     main] io.seata.config.FileConfiguration        : The file name of the operation is file.conf
14:23:52.027  INFO --- [                     main] io.seata.config.FileConfiguration        : The configuration file used is file.conf
# 初始化连接池
14:24:06.970  INFO --- [                     main] com.alibaba.druid.pool.DruidDataSource   : {
     dataSource-1} inited  
# 节点启动成功
14:24:11.591  INFO --- [                     main] i.s.core.rpc.netty.NettyServerBootstrap  : Server started, listen port: 28090
......
# 注册中心连接成功
14:24:42.806  INFO --- [                     main] com.netflix.discovery.DiscoveryClient    : Discovery Client initialized at timestamp 1611037482804 with initial instances count: 1
14:24:42.807  INFO --- [                     main] com.netflix.discovery.DiscoveryClient    : Saw local status change event StatusChangeEvent [timestamp=1611037482807, current=UP, previous=STARTING]
14:24:42.810  INFO --- [-InstanceInfoReplicator-0] com.netflix.discovery.DiscoveryClient    : DiscoveryClient_SEATA-TC-SERVER/192.168.0.120:seata-tc-server:28090: registering service...
14:24:42.936  INFO --- [-InstanceInfoReplicator-0] com.netflix.discovery.DiscoveryClient    : DiscoveryClient_SEATA-TC-SERVER/192.168.0.120:seata-tc-server:28090 - registration status: 204
浏览器访问Eureka注册中心,可以看到两个TC Server节点注册成功!

在这里插入图片描述

3. Seata AT 模式整合OpenFeign

Seata AT 模式是无侵入自动补偿的事务模式,所以本节就用它来解决Spring Cloud技术体系下多个微服务分布式事务的问题。微服务提供者提供Restful Http API 接口,服务消费者通过Feign进行Http调用,在调用链路中传递全局事务XID来实现分布式事务的传播。
我们以用户下单作为业务场景,主要包括订单服务、仓储服务、账户服务三个微服务,并且它们分别对应三个数据库。整体架构图如下:

Spring Cloud Alibaba 分布式事务 Seata 入门_第5张图片

3.1 初始化数据库

分别创建seata_order、seata_storage、seata_account三个数据库,因为使用的是Seata AT模式,所以每个数据库必须创建 undo_log 表,主要用于分支事务的回滚,sql脚本如下:

# 订单库
CREATE DATABASE seata_order;

CREATE TABLE seata_order.orders
(
    id               INT(11) NOT NULL AUTO_INCREMENT,
    product_id       INT(11)        DEFAULT NULL COMMENT '商品id',
    pay_amount       DECIMAL(10, 2) DEFAULT NULL COMMENT '支付金额',
    create_time      DATETIME       DEFAULT CURRENT_TIMESTAMP COMMENT '创建日期',
    last_update_time DATETIME       DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新日期',
    PRIMARY KEY (id)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;

CREATE TABLE seata_order.undo_log
(
    `branch_id`     BIGINT(20)   NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(100) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
    `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
    `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';

# 仓储库
CREATE DATABASE seata_storage;

CREATE TABLE seata_storage.product
(
    id               INT(11) NOT NULL AUTO_INCREMENT,
    stock            INT(11)  DEFAULT NULL COMMENT '库存数量',
    last_update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新日期',
    PRIMARY KEY (id)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;

# 初始化商品的库存
INSERT INTO seata_storage.product (id, stock) VALUES (1, 10); 

CREATE TABLE seata_storage.undo_log
(
    `branch_id`     BIGINT(20)   NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(100) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
    `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
    `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';

# 账户库
CREATE DATABASE seata_account;

CREATE TABLE seata_account.account
(
    id               INT(11) NOT NULL AUTO_INCREMENT COMMENT '账户id',
    balance          DECIMAL(10, 2)   DEFAULT NULL COMMENT '账户余额',
    last_update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新日期',
    PRIMARY KEY (id)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;

# 初始化账户金额
INSERT INTO seata_account.account (id, balance) VALUES (1,100);

CREATE TABLE seata_account.undo_log
(
    `branch_id`     BIGINT(20)   NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(100) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
    `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
    `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
3.2 搭建订单服务
整体项目结构如下:

Spring Cloud Alibaba 分布式事务 Seata 入门_第6张图片
1、pom文件添加seata依赖


<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.0modelVersion>
    <groupId>com.examplegroupId>
    <artifactId>seata-order-demoartifactId>
    <version>0.0.1-SNAPSHOTversion>
    <name>seata-order-demoname>
    <description>Demo project for Spring Bootdescription>

    <properties>
        <java.version>1.8java.version>
        <spring-boot-parent.version>2.3.0.RELEASEspring-boot-parent.version>
        <spring-cloud.version>Hoxton.SR9spring-cloud.version>
    properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-parentartifactId>
                <version>${spring-boot-parent.version}version>
                <type>pomtype>
                <scope>importscope>
            dependency>

            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-dependenciesartifactId>
                <version>${spring-cloud.version}version>
                <type>pomtype>
                <scope>importscope>
            dependency>
        dependencies>
    dependencyManagement>

    <dependencies>
        
        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-seataartifactId>
            <version>2.2.4.RELEASEversion>
        dependency>

        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
        dependency>

        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-openfeignartifactId>
        dependency>

        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>8.0.22version>
        dependency>

        
        <dependency>
            <groupId>org.mybatis.spring.bootgroupId>
            <artifactId>mybatis-spring-boot-starterartifactId>
            <version>2.1.4version>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
    dependencies>

    <build>
        <finalName>seata-order-demofinalName>
        <resources>
            <resource>
                <directory>src/main/javadirectory>
                <includes>
                    <include>com/**/dao/*.xmlinclude>
                includes>
            resource>
            <resource>
                <directory>src/main/resourcesdirectory>
                <includes>
                    <include>**/*.ymlinclude>
                    <include>**/*.propertiesinclude>
                    <include>**/*.xmlinclude>
                includes>
                <filtering>falsefiltering>
            resource>
        resources>
        <plugins>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
                <configuration>
                    <mainClass>com.example.seataorderdemo.SeataOrderDemoApplicationmainClass>
                configuration>
                
                <executions>
                    <execution>
                        <goals>
                            <goal>repackagegoal>
                        goals>
                    execution>
                executions>
            plugin>
            <plugin>
                <groupId>org.apache.maven.pluginsgroupId>
                <artifactId>maven-compiler-pluginartifactId>
                <configuration>
                    <source>1.8source>
                    <target>1.8target>
                    <encoding>UTF-8encoding>
                configuration>
            plugin>
        plugins>
    build>
project>
2、application.properties配置文件
spring.application.name=seata-order-demo
server.port=3010

# 注册中心地址
eureka.client.service-url.defaultZone=http://localhost:8761/eureka
# 客户端给eureka server发送心跳的频率
eureka.instance.lease-renewal-interval-in-seconds=10
# 客户端的ip注册到eureka server
eureka.instance.prefer-ip-address=true
# 自定义instance-id
eureka.instance.instance-id=${spring.cloud.client.ip-address}:${server.port}

# 连接数据库
spring.datasource.url=jdbc:mysql://192.168.0.118:3306/seata_order?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# 连接池名称
spring.datasource.hikari.pool-name=seata_order_demo_hikari_pool
# 最小空闲连接数
spring.datasource.hikari.minimum-idle=5
# 空闲连接存活最大时间,默认600000(10分钟)
spring.datasource.hikari.idle-timeout=180000
# 连接池中的最大连接数
spring.datasource.hikari.maximum-pool-size=20
# 客户端获取连接池连接的超时时长,默认30秒
spring.datasource.hikari.connection-timeout=30000
# 设置要执行的SQL查询,测试连接的有效性
spring.datasource.hikari.connection-test-query=SELECT 1

# 下划线的数据库字段映射成驼峰风格的实体对象字段
mybatis.configuration.map-underscore-to-camel-case=true

# 打印数据库日志
logging.level.com.example.seataorderdemo.dao=debug

# seata应用编号
seata.application-id=${spring.application.name}
# seata事务组编号
seata.tx-service-group=my_test_tx_group
# seata虚拟组映射
seata.service.vgroup-mapping.my_test_tx_group=seata-tc-server
# 指定注册中心的类型
seata.registry.type=eureka
# 注册中心的地址
seata.registry.eureka.service-url=http://localhost:8761/eureka
# 指定seata tc server集群的名字
seata.registry.eureka.application=seata-tc-server
这里需要特别注意的是,seata事务组编号要和TC Server的file.conf文件里面的事务组编号保持一致!!

Spring Cloud Alibaba 分布式事务 Seata 入门_第7张图片

3、OrderController 控制器

@RestController
@RequestMapping("/order")
public class OrderController {
     

    private Logger logger = LoggerFactory.getLogger(OrderController.class);

    @Autowired
    private OrderService orderService;

    @PostMapping("/createOrder")
    public void createOrder(Integer accountId, Integer productId,
                            BigDecimal payAmount, Integer buyNumber) {
     
        logger.info("生成订单,账户id:{},商品id:{},支付金额:{},购买数量:{}",
                accountId, productId, payAmount, buyNumber);
        orderService.createOrder(accountId, productId, payAmount, buyNumber);
    }
}
4、OrderService 接口
public interface OrderService {
     

    /**
     * 生成订单
     *
     * @param accountId 账户id
     * @param productId 商品id
     * @param payAmount 支付金额
     * @param buyNumber 购买数量
     */
    void createOrder(Integer accountId, Integer productId,
                     BigDecimal payAmount, Integer buyNumber);
}
5、OrderServiceImpl类,方法使用 @GlobalTransactional 注解声明全局事务
@Service
public class OrderServiceImpl implements OrderService {
     

    private Logger logger = LoggerFactory.getLogger(OrderServiceImpl.class);

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private StorageFeignClient storageFeignClient;

    @Autowired
    private AccountFeignClient accountFeignClient;

    @Override
    @GlobalTransactional(rollbackFor = Exception.class)
    public void createOrder(Integer accountId, Integer productId,
                            BigDecimal payAmount, Integer buyNumber) {
     
        logger.info("全局事务XID:{}", RootContext.getXID());

        // 扣减库存
        storageFeignClient.reduceStock(productId, buyNumber);

        // 扣减账户余额
        accountFeignClient.reduceBalance(accountId, payAmount);

        // 生成订单
        orderMapper.saveOrder(productId, payAmount);

        logger.info("订单生成成功!");
    }
}
6、OrderMapper 接口,保存订单
@Mapper
@Repository
public interface OrderMapper {
     

    /**
     * 保存订单
     *
     * @param productId 商品id
     * @param payAmount 支付金额
     * @return Integer
     */
    @Insert("INSERT INTO orders(product_id,pay_amount,create_time) " +
            "VALUES(#{productId},#{payAmount},NOW())")
    Integer saveOrder(@Param("productId") Integer productId,
                      @Param("payAmount") BigDecimal payAmount);
}
7、AccountFeignClient接口,使用feign声明式调用账户服务
@FeignClient("seata-account-demo")
public interface AccountFeignClient {
     
    /**
     * 扣减余额
     *
     * @param accountId 账户id
     * @param amount    扣减金额
     */
    @PostMapping("/account/reduceBalance")
    void reduceBalance(@RequestParam("accountId") Integer accountId,
                       @RequestParam("amount") BigDecimal amount);
}
8、StorageFeignClient接口,使用feign声明式调用仓储服务
@FeignClient("seata-storage-demo")
public interface StorageFeignClient {
     
    /**
     * 扣减库存
     *
     * @param productId 商品id
     * @param number    扣减数量
     */
    @PostMapping("/storage/reduceStock")
    void reduceStock(@RequestParam("productId") Integer productId,
                     @RequestParam("number") Integer number);
}
9、SeataOrderDemoApplication 启动类
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan(basePackages = "com.example.seataorderdemo.dao")
@EnableFeignClients
public class SeataOrderDemoApplication {
     

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

}
注解 作用
@EnableDiscoveryClient 开启服务发现,自动注册到注册中心
@MapperScan 自动扫描指定目录下面的Mapper接口
@EnableFeignClients 启用feign声明式调用
3.3 搭建仓储服务

整体项目结构如下:
Spring Cloud Alibaba 分布式事务 Seata 入门_第8张图片

1、pom文件请参考 【3.2 订单服务】里面的此文件 2、aplication.properties配置文件请参考 【3.2 订单服务】里面的此文件 3、StorageController 控制器
@RestController
@RequestMapping("/storage")
public class StorageController {
     

    private Logger logger = LoggerFactory.getLogger(StorageController.class);

    @Autowired
    private ProductService productService;

    @PostMapping("/reduceStock")
    public void reduceStock(Integer productId, Integer number) throws Exception {
     
        logger.info("扣减库存,商品id:{},数量:{}", productId, number);
        productService.reduceStock(productId, number);
    }
}
4、ProductService 接口
public interface ProductService {
     
    /**
     * 扣减库存数量
     *
     * @param productId 商品id
     * @param number    扣减数量
     * @throws Exception
     */
    void reduceStock(Integer productId, Integer number) throws Exception;
}
5、ProductServiceImpl 类,注解@Transactional表示开启分支事务
@Service
public class ProductServiceImpl implements ProductService {
     

    private Logger logger = LoggerFactory.getLogger(ProductServiceImpl.class);

    @Autowired
    private ProductDao productDao;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void reduceStock(Integer productId, Integer number) throws Exception {
     
        logger.info("全局事务XID:{}", RootContext.getXID());

        // 检查库存是否充足
        Integer stock = productDao.getProductStock(productId);
        if (number.compareTo(stock) > 0) {
     
            logger.error("库存不足,商品id:{},扣减数量:{}", productId, number);
            throw new Exception("库存不足");
        }

        // 更新库存数量
        Integer result = productDao.updateProductStock(productId, number);
        if (result <= 0) {
     
            logger.error("更新商品库存失败,商品id:{},扣减数量:{}", productId, number);
            throw new Exception("更新商品库存失败");
        }

        logger.info("更新商品库存成功,商品id:{},扣减数量:{}", productId, number);
    }
}
6、ProductDao 接口
@Mapper
@Repository
public interface ProductDao {
     

    /**
     * 查询商品的库存数量
     *
     * @param productId 商品id
     * @return Integer
     */
    @Select("SELECT stock FROM product where id=#{productId}")
    Integer getProductStock(@Param("productId") Integer productId);

    /**
     * 更新商品的库存数量
     *
     * @param productId 商品id
     * @param number    扣减数量
     * @return Integer
     */
    @Update("UPDATE product SET stock=stock-#{number} WHERE id=#{productId}")
    Integer updateProductStock(@Param("productId") Integer productId,
                               @Param("number") Integer number);
}
7、SeataStorageDemoApplication 启动类
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan(basePackages = "com.example.seatastoragedemo.dao")
public class SeataStorageDemoApplication {
     

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

}
3.4 搭建账户服务

整体项目结构图如下:
Spring Cloud Alibaba 分布式事务 Seata 入门_第9张图片

1、pom文件请参考 【3.2 订单服务】里面的此文件 2、aplication.properties配置文件请参考 【3.2 订单服务】里面的此文件 3、AccountController 控制器
@RestController
@RequestMapping("/account")
public class AccountController {
     

    private Logger logger = LoggerFactory.getLogger(AccountController.class);

    @Autowired
    private AccountService accountService;

    @PostMapping("/reduceBalance")
    public void reduceBalance(Integer accountId, BigDecimal amount) throws Exception {
     
        logger.info("扣减账户余额,账户Id:{},扣减金额:{}", accountId, amount);
        accountService.reduceBalance(accountId, amount);
    }
}
4、AccountService 接口
public interface AccountService {
     
    /**
     * 扣减账户余额
     *
     * @param accountId 账户id
     * @param amount    金额
     * @return Boolean
     */
    void reduceBalance(Integer accountId, BigDecimal amount) throws Exception;
}
5、AccountServiceImpl 类,注解@Transactional表示开启分支事务
@Service
public class AccountServiceImpl implements AccountService {
     

    private Logger logger = LoggerFactory.getLogger(AccountServiceImpl.class);

    @Autowired
    private AccountMapper accountMapper;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void reduceBalance(Integer accountId, BigDecimal amount) throws Exception {
     
        logger.info("全局事务XID:{}", RootContext.getXID());

        // 检查账户余额是否充足
        BigDecimal balance = accountMapper.getAccountBlance(accountId);
        if (amount.compareTo(balance) > 0) {
     
            logger.error("账户余额不足,扣减失败!accountId:{},amount:{}", accountId, amount);
            throw new Exception("账户余额不足!");
        }

        // 更新账户余额
        Integer result = accountMapper.updateAccountBlance(accountId, amount);
        if (result <= 0) {
     
            logger.error("更新账户余额失败!accountId:{},amount:{}", accountId, amount);
            throw new Exception("更新账户余额失败!");
        }

        logger.info("更新账户余额成功!accountId:{},amount:{}", accountId, amount);
    }
}
6、AccountMapper 接口
@Mapper
@Repository
public interface AccountMapper {
     

    /**
     * 查询账户余额
     *
     * @param accountId 账户id
     * @return BigDecimal
     */
    @Select("SELECT t.balance FROM account t WHERE t.id=#{accountId}")
    BigDecimal getAccountBlance(@Param("accountId") Integer accountId);

    /**
     * 更新账户余额
     *
     * @param accountId 账户id
     * @param amount    金额
     * @return Integer
     */
    @Update("UPDATE account SET balance=balance-#{amount} WHERE id=#{accountId}")
    Integer updateAccountBlance(@Param("accountId") Integer accountId,
                                @Param("amount") BigDecimal amount);
}
7、SeataAccountDemoApplication 启动类
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan(basePackages = "com.example.seataaccountdemo.dao")
public class SeataAccountDemoApplication {
     

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

}

4. 启动服务,开始测试

先后启动订单服务、仓储服务、账户服务,在IDEA 控制台可以看到seata初始化相关的日志,下面以启动订单服务打印的日志为例进行展示。
# seata 数据源代理
2021-01-21 12:50:49.037  INFO 15840 --- [           main] .s.s.a.d.SeataAutoDataSourceProxyCreator : Auto proxy of [dataSource]

# seata 全局事务扫描
2021-01-21 12:50:49.684  INFO 15840 --- [           main] i.s.s.a.GlobalTransactionScanner         : Bean[com.example.seataorderdemo.service.impl.OrderServiceImpl] with name [orderServiceImpl] would use interceptor [io.seata.spring.annotation.GlobalTransactionalInterceptor]

# 注册 Seata Resource Manager 到端口为28090的Seata TC Server 成功
2021-01-21 12:50:56.150  INFO 15840 --- [           main] i.s.c.r.netty.NettyClientChannelManager  : will connect to 192.168.0.120:28090
2021-01-21 12:50:56.150  INFO 15840 --- [           main] i.s.c.rpc.netty.RmNettyRemotingClient    : RM will register :jdbc:mysql://192.168.0.118:3306/seata_order
2021-01-21 12:50:56.157  INFO 15840 --- [           main] i.s.core.rpc.netty.NettyPoolableFactory  : NettyPool create channel to transactionRole:RMROLE,address:192.168.0.120:28090,msg:< RegisterRMRequest{
     resourceIds='jdbc:mysql://192.168.0.118:3306/seata_order', applicationId='seata-order-demo', transactionServiceGroup='my_test_tx_group'} >
2021-01-21 12:51:03.812  INFO 15840 --- [           main] i.s.c.rpc.netty.RmNettyRemotingClient    : register RM success. client version:1.3.0, server version:1.4.0,channel:[id: 0x7a38a004, L:/192.168.0.118:60927 - R:/192.168.0.120:28090]
2021-01-21 12:51:03.819  INFO 15840 --- [           main] i.s.core.rpc.netty.NettyPoolableFactory  : register success, cost 3205 ms, version:1.4.0,role:RMROLE,channel:[id: 0x7a38a004, L:/192.168.0.118:60927 - R:/192.168.0.120:28090]

# 注册 Seata Resource Manager 到端口为18090的Seata TC Server 成功
2021-01-21 12:51:03.820  INFO 15840 --- [           main] i.s.c.r.netty.NettyClientChannelManager  : will connect to 192.168.0.120:18090
2021-01-21 12:51:03.820  INFO 15840 --- [           main] i.s.c.rpc.netty.RmNettyRemotingClient    : RM will register :jdbc:mysql://192.168.0.118:3306/seata_order
2021-01-21 12:51:03.820  INFO 15840 --- [           main] i.s.core.rpc.netty.NettyPoolableFactory  : NettyPool create channel to transactionRole:RMROLE,address:192.168.0.120:18090,msg:< RegisterRMRequest{
     resourceIds='jdbc:mysql://192.168.0.118:3306/seata_order', applicationId='seata-order-demo', transactionServiceGroup='my_test_tx_group'} >
2021-01-21 12:51:05.406  INFO 15840 --- [           main] i.s.c.rpc.netty.RmNettyRemotingClient    : register RM success. client version:1.3.0, server version:1.4.0,channel:[id: 0x1a022a55, L:/192.168.0.118:60930 - R:/192.168.0.120:18090]
2021-01-21 12:51:05.406  INFO 15840 --- [           main] i.s.core.rpc.netty.NettyPoolableFactory  : register success, cost 1585 ms, version:1.4.0,role:RMROLE,channel:[id: 0x1a022a55, L:/192.168.0.118:60930 - R:/192.168.0.120:18090]

各个服务在eureka注册中心注册成功:
Spring Cloud Alibaba 分布式事务 Seata 入门_第10张图片

4.1 分布式事务正常提交测试
1、先看一下目前数据库各表的数据,如下图所示:

Spring Cloud Alibaba 分布式事务 Seata 入门_第11张图片

2、使用postman请求创建订单的接口

Spring Cloud Alibaba 分布式事务 Seata 入门_第12张图片

3、各个服务控制台打印日志

  • 订单服务
    Spring Cloud Alibaba 分布式事务 Seata 入门_第13张图片
  • 账户服务
    Spring Cloud Alibaba 分布式事务 Seata 入门_第14张图片
  • 仓储服务
    Spring Cloud Alibaba 分布式事务 Seata 入门_第15张图片
4、当前数据库各表的数据

Spring Cloud Alibaba 分布式事务 Seata 入门_第16张图片

4.2 分布式事务异常回滚测试
1、当前数据库各表的数据

Spring Cloud Alibaba 分布式事务 Seata 入门_第17张图片

2、使用postman生成订单,支付金额为1000元,账户余额不足会抛异常

Spring Cloud Alibaba 分布式事务 Seata 入门_第18张图片

3、查看各个服务控制台打印的日志
  • 订单服务
    在这里插入图片描述
  • 账户服务
    Spring Cloud Alibaba 分布式事务 Seata 入门_第19张图片
  • 仓储服务
    Spring Cloud Alibaba 分布式事务 Seata 入门_第20张图片

4、查看数据库各表的数据
Spring Cloud Alibaba 分布式事务 Seata 入门_第21张图片

5. 示例完整代码下载

spring-cloud-seata-demo

至此,Spring Cloud Alibaba 分布式事务 Seata 入门分享完毕

你可能感兴趣的:(Spring,Cloud,分布式,spring,cloud,mybatis)