我们在使用微服务架构的时候,难免会遇到跨服务操作数据库的情形。例如,创建订单的业务,我们需要在订单服务生成订单数据,同时要在商品服务进行库存扣减。此时的操作涉及到订单、商品两个服务的数据操作,如何保证同时成功或同时失败呢?对此,本篇将介绍使用 Seata 来解决此问题。

1. 关于 Seata

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。关于 Seata 的更多详细介绍,请参考官方文档(链接地址)。

2. 版本选择

由于我们 Spring Cloud Alibaba 选择的是 2022.0.0.0 版本,其中的 Seata 版本为 1.7.0,我们的 Seata 也选择版本为 1.7.0。

3. Seata Server 搭建

步骤一: 链接地址 下载服务端安装包 seata-server-1.7.0.tar.gz。

步骤二:使用 tar zxvf seata-server-1.7.0.tar.gz 命令解压。

步骤三:找到“/seata/script/server/db ”目录下的 mysql.sql 文件。创建数据库 mall-seata,并执行mysql.sql 文件脚本,初始化服务端表结构。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Cloud

步骤四:修改“/seata/conf”目录下的 application.yml 文件。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Cloud_02

将配置中心、注册中心都修改为 Nacos,内容如下。

server:
  port: 7091

spring:
  application:
    name: seata-server

logging:
  config: classpath:logback-spring.xml
  file:
    path: ${user.home}/logs/seata
  extend:
    logstash-appender:
      destination: 127.0.0.1:4560
    kafka-appender:
      bootstrap-servers: 127.0.0.1:9092
      topic: logback_to_logstash
      
console:
  user:
    username: seata
    password: seata

seata:
  config:
    # support: nacos 、 consul 、 apollo 、 zk  、 etcd3
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace: dev
      group: SEATA_GROUP
      username:
      password:
      context-path:
      ##if use MSE Nacos with auth, mutex with username/password attribute
      #access-key:
      #secret-key:
      data-id: seata-server.properties
  registry:
    # support: nacos 、 eureka 、 redis 、 zk  、 consul 、 etcd3 、 sofa
    type: nacos
    preferred-networks: 30.240.*
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
      namespace: dev
      cluster: default
      username:
      password:
      context-path:
      ##if use MSE Nacos with auth, mutex with username/password attribute
      #access-key:
      #secret-key:

  server:
    service-port: 8091 #If not configured, the default is '${server.port} + 1000'
    max-commit-retry-timeout: -1
    max-rollback-retry-timeout: -1
    rollback-retry-timeout-unlock-enable: false
    enable-check-auth: true
    enable-parallel-request-handle: true
    retry-dead-threshold: 130000
    xaer-nota-retry-timeout: 60000
    enableParallelRequestHandle: true
    recovery:
      committing-retry-period: 1000
      async-committing-retry-period: 1000
      rollbacking-retry-period: 1000
      timeout-retry-period: 1000
    undo:
      log-save-days: 7
      log-delete-period: 86400000
    session:
      branch-async-queue-size: 5000 #branch async remove queue size
      enable-branch-async-remove: false #enable to asynchronous remove branchSession
   #store:
    # support: file 、 db 、 redis
    #mode: db
  metrics:
    enabled: false
    registry-type: compact
    exporter-list: prometheus
    exporter-prometheus-port: 9898
  transport:
    rpc-tc-request-timeout: 15000
    enable-tc-server-batch-send-response: false
    shutdown:
      wait: 3
    thread-factory:
      boss-thread-prefix: NettyBoss
      worker-thread-prefix: NettyServerNIOWorker
      boss-thread-size: 1
#  server:
#    service-port: 8091 #If not configured, the default is '${server.port} + 1000'
  security:
    secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
    tokenValidityInMilliseconds: 1800000
    ignore:
      urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login

上面的 seata-server.properties 文件,从 Nacos 中读取,用以配置分组和会话存储,内容如下。

#Transaction routing rules configuration, only for the client
service.vgroupMapping.my_test_tx_group=default
#If you use a registry, you can ignore it
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false

#Transaction storage configuration, only for the server. The file, db, and redis configuration values are optional.
store.mode=db
store.lock.mode=db
store.session.mode=db
#Used for password encryption

#These configurations are required if the `store mode` is `db`. If `store.mode,store.lock.mode,store.session.mode` are not equal to `db`, you can remove the configuration block.
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/mall-seata?rewriteBatchedStatements=true
store.db.user=root
store.db.password=root
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.distributedLockTable=distributed_lock
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000

步骤五:进入到 “/seata/bin”目录,执行 “./seata-server.sh -p 8091 -h 127.0.0.1 -m db”命令即可启动服务。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Cloud_03

步骤六:检查服务启动情况,执行“cat /soft/seata/logs/start.out ”,看到启动日志没报错,说明服务已经起来了。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_COLA_04

打开 Nacos 服务列表,看到 seata-server 也注册上来了。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Cloud Alibaba_05

浏览器输入“ http://127.0.0.1:7091”地址,访问 Seata 控制台。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Cloud Alibaba_06

输入用户名:seata,密码:seata,登录结果如下。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_分布式事务-Seata_07

4. Seata 整合

在 Nacos 上创建 Seata 客户端配置文件,名称为:seata-client.yaml,内容如下。

seata:
  enabled: true
  # 指定事务分组至集群映射关系,集群名default需要与seata-server注册到Nacos的cluster保持一致
  service:
    vgroupMapping:
      my_test_tx_group: default
    default:
      grouplist: ${SEATA_SERVER_ADDR}
    enableDegrade: false
    disableGlobalTransaction: false
    # 事务分组配置
  tx-service-group: my_test_tx_group

其中 ${SEATA_SERVER_ADDR} 为 seata-server 的地址,例如:127.0.0.1:8091。

账户模块、商品模块、订单模块的 domain 工程添加如下 Seata 依赖。

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

      账户模块、商品模块、订单模块 start 工程的 bootstrap.yml 文件添加 seata-client.yaml 引用。

- data-id: seata-client.yaml
  group: mall
  refresh: true

5. 需求场景

目前我们有账户、商品、订单三个服务,我们来测试一下这样的场景,在订单服务新增创建订单的方法,创建订单时,同时调用商品服务扣减库存方法和调用账户服务扣减余额方法,交互时序图如下。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_分布式事务-Seata_08

6. 需求实现

6.1. 商品服务新增扣减库存方法。

6.1.1. mall-product-center-client 工程

ProductFeignClient 新增的代码如下。

@Operation(summary = "根据商品code扣减数量")
    @PostMapping("/reduceCountByCode")
    @Parameters({
            @Parameter(name = "productCode",description = "商品code",required = true),
            @Parameter(name = "count",description = "数量",required = true)
    })
    void reduceCountByCode(@RequestParam("productCode") String productCode, @RequestParam("count") Integer count) throws BizException;

ProductClientController 新增的代码如下。

@Override
    public void reduceCountByCode(String productCode, Integer count) throws BizException {
        this.productExecutor.reduceProductCountByCode(productCode,count);
    }

6.1.2. mall-product-center-app 工程

ProductExecutor 新增的代码如下。

/**
     * 根据商品code扣减库存
     */
    public void reduceProductCountByCode(String productCode, Integer count) throws BizException {
        this.productService.reduceProductCountByCode(productCode,count);
    }

6.1.3. mall-product-center-domain 工程

ProductService 新增的代码如下。

/**
     * 根据商品code扣减库存
     */
    void reduceProductCountByCode(String productCode, Integer count) throws BizException;

6.1.4. mall-product-center-infra 工程

ProductServiceImpl 新增的代码如下。

@Override
    public void reduceProductCountByCode(String productCode, Integer count) throws BizException {
        LambdaQueryWrapper<ProductEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(ProductEntity::getProductCode,productCode);
        ProductEntity productEntity = this.getOne(lambdaQueryWrapper);
        if(Objects.isNull(productEntity)){
            throw ExceptionFactory.bizException("商品信息不存在!");
        }
        productEntity.setCount(productEntity.getCount()-count);
        productEntity.setUpdatedBy(1L);
        productEntity.setUpdatedTime(new Date());
        this.updateById(productEntity);
    }

6.2. 账户服务新增扣减余额方法。

6.2.1. mall-account-center-client 工程

新增 AccountFeignClient、AccountFeignClientFallback、AccountFeignClientFallbackFactory 三个类,项目结构如下。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Cloud_09

AccountFeignClient 代码如下。

@FeignClient(name = BaseConstant.Domain.ACCOUNT,fallbackFactory = AccountFeignClientFallbackFactory.class)
public interface AccountFeignClient {

    @Operation(summary = "根据账户code扣减余额")
    @PostMapping("/reduceAmountByCode")
    @Parameters({
            @Parameter(name = "accountCode",description = "账户code",required = true),
            @Parameter(name = "amount",description = "金额",required = true)
    })
    void reduceAmountByCode(@RequestParam("accountCode") String accountCode, @RequestParam("amount") BigDecimal amount) throws BizException;
}

其中,BaseConstant.Domain.ACCOUNT 值为:mall-account-service。

AccountFeignClientFallback 代码如下。

@Component
public class AccountFeignClientFallback implements AccountFeignClient {

    @Override
    public void reduceAmountByCode(String accountCode, BigDecimal amount) throws BizException {
        throw ExceptionFactory.bizException("扣减余额失败,方法:reduceCountByCode,参数:accountCode="+accountCode+",amount="+amount);
    }
}

AccountFeignClientFallbackFactory 代码如下。

@Component
public class AccountFeignClientFallbackFactory implements FallbackFactory<AccountFeignClient> {
    @Override
    public AccountFeignClient create(Throwable cause) {
        cause.printStackTrace();
        return new AccountFeignClientFallback();
    }
}

6.2.2. mall-account-center-adapter 工程

在 org.example.account.adapter.client 包路径下,新增 AccountClientController类,代码如下。

@Slf4j
@Tag(name = "account-client端api")
@RestController
public class AccountClientController implements AccountFeignClient {

    @Resource
    private AccountExecutor accountExecutor;

    @Override
    public void reduceAmountByCode(String accountCode, BigDecimal amount) throws BizException {
        this.accountExecutor.reduceAccountAmountByCode(accountCode,amount);
    }
}

6.2.3. mall-account-center-app 工程

在 AccountExecutor 类中,增加根据账户code扣减余额的方法,代码如下。

/**
     *  根据账户code扣减余额
     */
    public void reduceAccountAmountByCode(String accountCode, BigDecimal amount) throws BizException {
       this.accountService.reduceAmountByCode(accountCode,amount);
    }

6.2.4. mall-account-center-domain 工程

在 AccountService 类中,增加根据账户code扣减余额的方法,代码如下。

/**
     * 根据账户code扣减余额
     */
    void reduceAmountByCode(String accountCode, BigDecimal amount) throws BizException;

6.2.5. mall-account-center-infra 工程

在 AccountServiceImpl 类中,增加根据账户code扣减余额的方法,代码如下。

@Override
    public void reduceAmountByCode(String accountCode, BigDecimal amount) throws BizException {
        LambdaQueryWrapper<AccountEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(AccountEntity::getAccountCode,accountCode);
        AccountEntity accountEntity = this.getOne(lambdaQueryWrapper);
        if(Objects.isNull(accountEntity)){
            throw ExceptionFactory.bizException("账户信息不存在!");
        }
        accountEntity.setAmount(accountEntity.getAmount().subtract(amount));
        accountEntity.setUpdatedBy(1L);
        accountEntity.setUpdatedTime(new Date());
        this.updateById(accountEntity);
    }

6.3. 订单服务新增创建订单方法。

6.3.1. mall-order-center-adapter 工程

在 OrderController 类中,新增创建订单的方法,代码如下。

@Operation(summary = "创建订单")
    @PostMapping("/createOrder")
    public ResponseResult<String> createOrder(@RequestBody OrderReqDTO orderReqDTO){
        log.info("createOrder order, orderReqDTO:{}",orderReqDTO);
        this.orderExecutor.createOrder(orderReqDTO);
        return ResponseResult.ok("createOrder Order succeed");
    }

6.3.2. mall-order-center-app 工程

在 OrderExecutor 类中国,新增创建订单的方法,代码如下。

@Component
public class OrderExecutor {

    @Resource
    private OrderService orderService;
    @Resource
    private ProductFeignClient productFeignClient;
    @Resource
    private AccountFeignClient accountFeignClient;

    /**
     * 省略其他方法
     */
    

    /**
     * 创建订单
     */
    public void createOrder(OrderReqDTO orderReqDTO) throws BaseException {
        this.addOrder(orderReqDTO);
        this.productFeignClient.reduceCountByCode(orderReqDTO.getProductCode(),orderReqDTO.getCount());
        this.accountFeignClient.reduceAmountByCode(orderReqDTO.getAccountCode(),orderReqDTO.getAmount());
    }
}

6.4. 传统事务

6.4.1. 代码实现

传统事务处理方式,是在 org.example.order.app.executor.OrderExecutor#createOrder 方法上添加 @Transactional 注解,代码如下。

@Transactional
public void createOrder(OrderReqDTO orderReqDTO) throws BaseException {
        this.addOrder(orderReqDTO);
        this.productFeignClient.reduceCountByCode(orderReqDTO.getProductCode(),orderReqDTO.getCount());
        this.accountFeignClient.reduceAmountByCode(orderReqDTO.getAccountCode(),orderReqDTO.getAmount());
    }

此时,在 org.example.account.infra.impl.AccountServiceImpl#reduceAccountAmountByCode 方法故意加入异常代码,如下所示。

public void reduceAccountAmountByCode(String accountCode, BigDecimal amount) throws BizException {
        int a = 1/0;
        LambdaQueryWrapper<AccountEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(AccountEntity::getAccountCode,accountCode);
        AccountEntity accountEntity = this.accountService.getOne(lambdaQueryWrapper);
        if(Objects.isNull(accountEntity)){
            throw ExceptionFactory.bizException("账户信息不存在!");
        }
        accountEntity.setAmount(accountEntity.getAmount().subtract(amount));
        accountEntity.setUpdatedBy(1L);
        accountEntity.setUpdatedTime(new Date());
        this.accountService.updateById(accountEntity);
    }

6.4.2. 功能测试

启动账户模块、商品模块、订单模块的服务。测试前,mall_account 表中,account_code = '1000002' 的记录,账户余额为 1000.00;mall_product 表中,product_code='100001' 的记录,库存数量为 10;mall_order 表中记录为空。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_分布式事务-Seata_10

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_COLA_11

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Cloud_12

此时,前端输入以下参数发起请求,则出现结果报错。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Cloud_13

正是 org.example.account.infra.impl.AccountServiceImpl#reduceAccountAmountByCode 方报的错。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Boot3_14

此时查看账户金额,仍未扣减。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Boot3_15

查看订单数据,依旧为空。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Cloud Alibaba_16

查看商品库存,数量却已扣减为 9。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Boot3_17

上面的测试结果,数据一致性已经丢失。

6.5. AT 模式

6.5.1. 代码实现

分别在 mall-account、mall-product、mall-order 数据库中,添加 undo_log 表,sql 脚本如下。

-- 注意此处0.7.0+ 增加字段 context
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;

Seata 默认的就是 AT 模式,我们只需要将 org.example.order.app.executor.OrderExecutor#createOrder 方法上的 @Transactional 注解改成 @GlobalTransactional 注解即可,如下所示。

@GlobalTransactional
 public void createOrder(OrderReqDTO orderReqDTO) throws BaseException {
        this.addOrder(orderReqDTO);
        this.productFeignClient.reduceCountByCode(orderReqDTO.getProductCode(),orderReqDTO.getCount());
        this.accountFeignClient.reduceAmountByCode(orderReqDTO.getAccountCode(),orderReqDTO.getAmount());
}

6.5.2. 功能测试

先将 mall_product 表中 product_code='100001' 的记录,库存数量改回 10。

重启订单服务,前端再用以下参数发起请求,同样出现结果报错。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_分布式事务-Seata_18

此时查看账户金额,仍未扣减。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Cloud Alibaba_19

查看订单数据,依旧为空。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Boot3_20

查看商品库存,数量仍旧为 10。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Cloud_21

上面的测试结果,数据一致性始终保持一致。

undo_log 表的作用是,一阶段插入回滚日志,二阶段提交时异步删除回归日志。

可以在 reduceAmountByCode 方法上设置断点,查看 undo_log 的执行情况。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Cloud Alibaba_22

当程序执行到方法断点时,mall-product、mall-order 数据库中的 undo_log 表,都会被插入回滚日志,如下所示。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_分布式事务-Seata_23

当方法执行完,则 undo_log 表的回滚日志将被删除。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_COLA_24

6.6. TCC 模式

TCC 需要为每个事务方法增加 commit 和 cancel 的方法。

6.6.1. 代码实现

6.6.1.1. 账户模块扣减余额方法实现
6.6.1.1.1. mall-account-center-domain 工程

创建 org.example.account.domain.action 包目录,在该目录下创建 AccountTccAction 接口,代码如下。

@LocalTCC
public interface AccountTccAction {

    /**
     * 根据账户code扣减余额
     */
    @TwoPhaseBusinessAction(name = "tcc-reduce-amount-by-code", commitMethod = "reduceAmountByCodeCommit", rollbackMethod = "reduceAmountByCodeCancel")
    void reduceAmountByCode(@BusinessActionContextParameter(paramName = "accountCode") String accountCode, @BusinessActionContextParameter(paramName = "amount") BigDecimal amount);

    /**
     *  根据账户code扣减余额-提交
     */
    boolean reduceAmountByCodeCommit(BusinessActionContext businessActionContext);

    /**
     * 根据商品code扣减库存-回滚
     */
    boolean reduceAmountByCodeCancel(BusinessActionContext businessActionContext);

}
6.6.1.1.2. mall-account-center-infra 工程

创建 AccountTccAction 接口的实现类 AccountTccActionImpl,代码如下。

@Slf4j
@Component
public class AccountTccActionImpl implements AccountTccAction {

    @Resource
    private AccountService accountService;

    @Override
    public void reduceAmountByCode(String accountCode, BigDecimal amount) {
        this.accountService.reduceAmountByCode(accountCode,amount);
    }

    @Override
    public boolean reduceAmountByCodeCommit(BusinessActionContext businessActionContext) {
        String accountCode = businessActionContext.getActionContext("accountCode")+"";
        BigDecimal amount = new BigDecimal(businessActionContext.getActionContext("amount") + "");
        log.info("reduceAmountByCodeCommit--------accountCode={},amount={}", accountCode,amount);
        return true;
    }

    @Override
    public boolean reduceAmountByCodeCancel(BusinessActionContext businessActionContext) {
        String accountCode = businessActionContext.getActionContext("accountCode")+"";
        BigDecimal amount = new BigDecimal(businessActionContext.getActionContext("amount") + "");
        log.info("reduceAmountByCodeCancel--------accountCode={},amount={}", accountCode,amount);
        this.accountService.reduceAmountByCode(accountCode,amount.negate());
        return true;
    }
}
6.6.1.1.3. mall-account-center-client 工程

给 AccountFeignClient 接口添加方法如下。

@Operation(summary = "根据账户code扣减余额-TCC")
    @PostMapping("/reduceAmountByCodeTcc")
    @Parameters({
            @Parameter(name = "accountCode",description = "账户code",required = true),
            @Parameter(name = "amount",description = "金额",required = true)
    })
    void reduceAmountByCodeTcc(@RequestParam("accountCode") String accountCode, @RequestParam("amount") BigDecimal amount) throws BizException;

AccountFeignClientFallback 添加方法如下。

@Override
    public void reduceAmountByCodeTcc(String accountCode, BigDecimal amount) throws BizException {
        throw ExceptionFactory.bizException("reduceAmountByCodeTcc-扣减余额失败,方法:reduceCountByCode,参数:accountCode="+accountCode+",amount="+amount);
    }
6.6.1.1.4. mall-account-center-adapter 工程

给 AccountClientController 添加方法如下。

@Override
    public void reduceAmountByCodeTcc(String accountCode, BigDecimal amount) throws BizException {
        this.accountExecutor.reduceAccountAmountByCodeTcc(accountCode,amount);
    }
6.6.1.1.5. mall-account-center-app工程

给 AccountExecutor 添加方法如下。

@Component
public class AccountExecutor {

    @Resource
    private AccountService accountService;
    @Resource
    private AccountTccAction accountTccAction;

    /**
     * 省略其他方法
     */
   
    /**
     * 根据账户code扣减余额-TCC
     */
    public void reduceAccountAmountByCodeTcc(String accountCode, BigDecimal amount) throws BizException {
        this.accountTccAction.reduceAmountByCode(accountCode,amount);
    }
}
6.6.1.2. 商品模块扣减库存方法实现
6.6.1.2.1. mall-product-center-domain 工程

创建 org.example.product.domain.action 包目录,在该目录下创建 ProductTccAction 接口,代码如下。

@LocalTCC
public interface ProductTccAction {

    /**
     * 根据商品code扣减库存
     */
    @TwoPhaseBusinessAction(name = "tcc-reduce-count-by-code", commitMethod = "reduceCountByCodeCommit", rollbackMethod = "reduceCountByCodeCancel")
    void reduceCountByCode(@BusinessActionContextParameter(paramName = "productCode") String productCode, @BusinessActionContextParameter(paramName = "count") Integer count);

    /**
     * 根据商品code扣减库存-提交
     */
    boolean reduceCountByCodeCommit(BusinessActionContext businessActionContext);

    /**
     *  根据商品code扣减库存-回滚
     */
    boolean reduceCountByCodeCancel(BusinessActionContext businessActionContext);
}
6.6.1.2.2. mall-product-center-infra 工程

创建 ProductTccAction 接口的实现类 ProductTccActionImpl,代码如下。

@Slf4j
@Component
public class ProductTccActionImpl implements ProductTccAction {

    @Resource
    private ProductService productService;

    @Override
    public void reduceCountByCode(String productCode, Integer count) {
        this.productService.reduceProductCountByCode(productCode,count);
    }

    @Override
    public boolean reduceCountByCodeCommit(BusinessActionContext businessActionContext) {
        String productCode = businessActionContext.getActionContext("productCode")+"";
        Integer count = Integer.getInteger(businessActionContext.getActionContext("count") + "");
        log.info("reduceCountByCodeCommit--------productCode={},count={}", productCode,count);
        return true;
    }

    @Override
    public boolean reduceCountByCodeCancel(BusinessActionContext businessActionContext) {
        String productCode = businessActionContext.getActionContext("productCode")+"";
        Integer count = Integer.valueOf(businessActionContext.getActionContext("count") + "");
        log.info("reduceCountByCodeCancel--------productCode={},count={}", productCode,count);
        this.productService.reduceProductCountByCode(productCode,-count);
        return true;
    }
}
6.6.1.2.3. mall-product-center-client 工程

给 ProductFeignClient 接口添加方法如下。

@Operation(summary = "根据商品code扣减数量-TCC")
    @PostMapping("/reduceCountByCodeTcc")
    @Parameters({
            @Parameter(name = "productCode",description = "商品code",required = true),
            @Parameter(name = "count",description = "数量",required = true)
    })
    void reduceCountByCodeTcc(@RequestParam("productCode") String productCode, @RequestParam("count") Integer count) throws BizException;

ProductFeignClientFallback 添加方法如下。

@Override
    public void reduceCountByCodeTcc(String productCode, Integer count) throws BizException {
        throw ExceptionFactory.bizException("reduceCountByCodeTcc-扣减库存失败,方法:reduceCountByCode,参数:productCode="+productCode+",count="+count);
    }
6.6.1.2.4. mall-product-center-adapter 工程

给 ProductClientController 添加方法如下。

@Override
    public void reduceCountByCodeTcc(String productCode, Integer count) throws BizException {
        this.productExecutor.reduceProductCountByCodeTcc(productCode,count);
    }
6.6.1.2.5. mall-product-center-app工程

给 ProductExecutor 添加方法如下。

@Component
public class ProductExecutor {

    @Resource
    private ProductService productService;
    @Resource
    private ProductTccAction productTccAction;

    /**
     * 省略其他方法
     */

    /**
     *  根据商品code扣减库存-TCC
     */
    public void reduceProductCountByCodeTcc(String productCode, Integer count) throws BizException {
        this.productTccAction.reduceCountByCode(productCode,count);
    }

}
6.6.1.3. 订单模块创建订单方法实现
6.6.1.3.1. mall-order-center-domain 工程

创建 org.example.order.domain.action 包目录,在该目录下创建 OrderTccAction 接口,代码如下。

@LocalTCC
public interface OrderTccAction {

    /**
     *  创建订单
     */
    @TwoPhaseBusinessAction(name = "tcc-create-order", commitMethod = "createOrderCommit", rollbackMethod = "createOrderCancel")
    void createOrder(OrderReqDTO orderReqDTO,@BusinessActionContextParameter(paramName = "id")  Long id) throws BaseException;

    /**
     * 创建订单-提交
     */
    boolean createOrderCommit(BusinessActionContext businessActionContext);

    /**
     *  创建订单-回滚
     */
    boolean createOrderCancel(BusinessActionContext businessActionContext);

}
6.6.1.3.2. mall-order-center-infra 工程

创建 OrderTccAction 接口的实现类 OrderTccActionImpl,代码如下。

@Slf4j
@Component
public class OrderTccActionImpl implements OrderTccAction {

    @Resource
    private OrderService orderService;


    @Override
    public void createOrder(OrderReqDTO orderReqDTO, Long id) throws BaseException {
        this.orderService.add(orderReqDTO,id);
    }

    @Override
    public boolean createOrderCommit(BusinessActionContext businessActionContext) {
        Long id = Long.parseLong(businessActionContext.getActionContext("id") + "");
        log.info("createOrderCommit--------id={}",id);
        return true;
    }

    @Override
    public boolean createOrderCancel(BusinessActionContext businessActionContext) {
        Long id = Long.parseLong(businessActionContext.getActionContext("id") + "");
        log.info("createOrderCancel--------id={}",id);
        return this.orderService.removeById(id);
    }
}
6.6.1.3.3. mall-order-center-adapter 工程

在 OrderController 控制器中,将 createOrder 接口调用创建订单的方法改为调用 createOrderTcc,。

@Operation(summary = "创建订单")
    @PostMapping("/createOrder")
    public ResponseResult<String> createOrder(@RequestBody OrderReqDTO orderReqDTO){
        log.info("createOrder order, orderReqDTO:{}",orderReqDTO);
        this.orderExecutor.createOrderTcc(orderReqDTO);
        return ResponseResult.ok("createOrder Order succeed");
    }
6.6.1.3.4. mall-order-center-app工程

给 OrderExecutor 添加方法如下。

@Component
public class OrderExecutor {

    @Resource
    private OrderService orderService;
    @Resource
    private OrderTccAction orderTccAction;
    @Resource
    private ProductFeignClient productFeignClient;
    @Resource
    private AccountFeignClient accountFeignClient;

    /**
     *  省略其他方法
     */

    /**
     *  创建订单-TCC
     */
    @GlobalTransactional
    public void createOrderTcc(OrderReqDTO orderReqDTO) throws BaseException {
        Long id = IdWorker.getId();
        this.orderTccAction.createOrder(orderReqDTO,id);
        this.productFeignClient.reduceCountByCodeTcc(orderReqDTO.getProductCode(),orderReqDTO.getCount());
        this.accountFeignClient.reduceAmountByCodeTcc(orderReqDTO.getAccountCode(),orderReqDTO.getAmount());
        int a = 1/0;
    }
}

注意:这里将“int a = 1/0;”异常代码放在 createOrderTcc 方法中,移除 org.example.account.infra.impl.AccountServiceImpl#reduceAmountByCode 方法中的“int a = 1/0;”。

6.6.2. 功能测试

启动账户模块、商品模块、订单模块的服务。测试前,mall_account 表中,account_code = '1000002' 的记录,账户余额为 1000.00;mall_product 表中,product_code='100001' 的记录,库存数量为 10;mall_order 表中记录为空。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Boot3_25

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Cloud Alibaba_26

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Cloud Alibaba_27

前端输入以下参数发起请求,则出现结果报错。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Cloud Alibaba_28

报错日志如下所示。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_分布式事务-Seata_29

查询账户表如下,账户余额为 1000.00。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Boot3_30

查询商品表如下,库存数量仍为 10。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Cloud_31

查询订单表如下,记录仍为空。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Boot3_32

sql 执行日志如下。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_分布式事务-Seata_33

从上面的测试结果和 sql 执行日志中可以看出,TCC 事务提交失败时,通过补偿保证数据的一致性。TCC 与 AT 的回滚区别就在于,AT 是从 undo_log 表中的镜像进行回滚;而 TCC 是通过我们自定义的方法(reduceAmountByCodeCancel、reduceCountByCodeCancel、createOrderCancel)进行回滚。

6.6.3. 幂等、悬挂和空回滚问题处理

TCC 模式中存在幂等、悬挂和空回滚三大问题。在 Seata1.5.1 之前的版本,需要开发者自己考虑解决方法进行处理,借助 undo_log 表或 redis 都可以处理。在 Seata1.5.1 版本开始,Seata 已经解提供了幂等、悬挂和空回滚问题的解决方案,需要在 @TwoPhaseBusinessAction 上增加 useTCCFence = true 属性,同时在对应数据库中增加 tcc_fence_log 表。tcc_fence_log 表的 sql 脚本如下。

CREATE TABLE IF NOT EXISTS `tcc_fence_log`
(
  `xid`           VARCHAR(128)  NOT NULL COMMENT 'global id',
  `branch_id`     BIGINT        NOT NULL COMMENT 'branch id',
  `action_name`   VARCHAR(64)   NOT NULL COMMENT 'action name',
  `status`        TINYINT       NOT NULL COMMENT 'status(tried:1;committed:2;rollbacked:3;suspended:4)',
  `gmt_create`    DATETIME(3)   NOT NULL COMMENT 'create time',
  `gmt_modified`  DATETIME(3)   NOT NULL COMMENT 'update time',
  PRIMARY KEY (`xid`, `branch_id`),
  KEY `idx_gmt_modified` (`gmt_modified`),
  KEY `idx_status` (`status`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

我们使用的是 Spring Cloud Alibaba 2022.0.0.0 版本,其中的 Seata 版本为 1.7.0,通过使用 useTCCFence = true 属性和增加 tcc_fence_log 表即可。

6.7. Saga 模式

在 Seata 中,目前 Saga 模式是使用状态机方式来实现的。因此,我们下面的下单流程,使用状态机方式实现。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Cloud_34

6.7.1. 代码实现

6.7.1.1. 账户模块扣减余额方法实现
6.7.1.1.1. mall-account-center-domain工程

在 org.example.account.domain.action 目录下添加 AccountSagaAction 类。代码如下。

public interface AccountSagaAction {

    /**
     * 根据账户code扣减余额
     */
    boolean reduceAmountByCode(String accountCode, BigDecimal amount);

    /**
     * 根据账户code扣减余额-方法补偿
     */
    boolean compensateReduceAmountByCode(String accountCode, BigDecimal amount);
}
6.7.1.1.2. mall-account-center-infra工程

在 org.example.account.infra.impl 目录下添加 AccountSagaActionImpl 类。代码如下。

@Slf4j
@Service
public class AccountSagaActionImpl implements AccountSagaAction {

    @Resource
    private AccountService accountService;

    @Override
    public boolean reduceAmountByCode(String accountCode, BigDecimal amount) {
        log.info("reduceAmountByCode--------accountCode={},amount={}", accountCode,amount);
        this.accountService.reduceAmountByCode(accountCode,amount);
        return true;
    }

    @Override
    public boolean compensateReduceAmountByCode(String accountCode, BigDecimal amount) {
        log.info("compensateReduceAmountByCode--------accountCode={},amount={}", accountCode,amount);
        this.accountService.reduceAmountByCode(accountCode,amount.negate());
        return true;
    }
}
6.7.1.1.3. mall-account-center-client 工程

给 AccountFeignClient 接口添加方法如下。

@Operation(summary = "根据账户code扣减余额-Saga")
    @PostMapping("/reduceAmountByCodeSaga")
    @Parameters({
            @Parameter(name = "accountCode",description = "账户code",required = true),
            @Parameter(name = "amount",description = "金额",required = true)
    })
    void reduceAmountByCodeSaga(@RequestParam("accountCode") String accountCode, @RequestParam("amount") BigDecimal amount) throws BizException;

    @Operation(summary = "补偿-根据账户code扣减余额-Saga")
    @PostMapping("/compensateReduceAmountByCodeSaga")
    @Parameters({
            @Parameter(name = "accountCode",description = "账户code",required = true),
            @Parameter(name = "amount",description = "金额",required = true)
    })
    void compensateReduceAmountByCodeSaga(@RequestParam("accountCode") String accountCode, @RequestParam("amount") BigDecimal amount) throws BizException;

AccountFeignClientFallback 添加方法如下。

@Override
    public void reduceAmountByCodeSaga(String accountCode, BigDecimal amount) throws BizException {
        throw ExceptionFactory.bizException("reduceAmountByCodeSaga-扣减余额失败,方法:reduceCountByCode,参数:accountCode="+accountCode+",amount="+amount);
    }

    @Override
    public void compensateReduceAmountByCodeSaga(String accountCode, BigDecimal amount) throws BizException {
        throw ExceptionFactory.bizException("compensateReduceAmountByCodeSaga-补偿扣减余额失败,方法:reduceCountByCode,参数:accountCode="+accountCode+",amount="+amount);
    }
6.7.1.1.4. mall-account-center-adapter 工程

给 AccountClientController 添加方法如下。

@Override
    public void reduceAmountByCodeSaga(String accountCode, BigDecimal amount) throws BizException {
        this.accountExecutor.reduceAccountAmountByCodeSaga(accountCode,amount);
    }

    @Override
    public void compensateReduceAmountByCodeSaga(String accountCode, BigDecimal amount) throws BizException {
        this.accountExecutor.compensateReduceAccountAmountByCodeSaga(accountCode,amount);
    }
6.7.1.1.5. mall-account-center-app工程

给 AccountExecutor 添加方法如下。

@Component
public class AccountExecutor {

    @Resource
    private AccountService accountService;
    @Resource
    private AccountTccAction accountTccAction;
    @Resource
    private AccountSagaAction accountSagaAction;

    /**
     * 省略其他方法
     */
   
    /**
     * 根据账户code扣减余额-Saga
     */
    public void reduceAccountAmountByCodeSaga(String accountCode, BigDecimal amount) throws BizException {
        this.accountSagaAction.reduceAmountByCode(accountCode,amount);
    }

    /**
     *  补偿-根据账户code扣减余额-Saga
     */
    public void compensateReduceAccountAmountByCodeSaga(String accountCode, BigDecimal amount) throws BizException {
        this.accountSagaAction.compensateReduceAmountByCode(accountCode,amount);
    }
}
6.7.1.2. 商品模块扣减库存方法实现
6.7.1.2.1. mall-product-center-domain工程

在 org.example.product.domain.action 目录下添加 ProductSagaAction 类。代码如下。

public interface ProductSagaAction {

    /**
     *  根据商品code扣减库存
     */
    void reduceCountByCode(String productCode, Integer count);

    /**
     *  根据商品code扣减库存-方法补偿
     */
    void compensateReduceCountByCode(String productCode, Integer count);
}
6.7.1.2.2. mall-product-center-infra工程

在 org.example.product.infra.impl 目录下添加 ProductSagaActionImpl 类。代码如下。

@Slf4j
@Service
public class ProductSagaActionImpl implements ProductSagaAction {

    @Resource
    private ProductService productService;

    @Override
    public void reduceCountByCode(String productCode, Integer count) {
        log.info("reduceCountByCode--------productCode={},count={}", productCode,count);
        this.productService.reduceProductCountByCode(productCode,count);
    }

    @Override
    public void compensateReduceCountByCode(String productCode, Integer count) {
        log.info("reduceCountByCodeCancel--------productCode={},count={}", productCode,count);
        this.productService.reduceProductCountByCode(productCode,-count);
    }
}
6.7.1.2.3. mall-product-center-client 工程

给 ProductFeignClient 接口添加方法如下。

@Operation(summary = "根据商品code扣减数量-Saga")
    @PostMapping("/reduceCountByCodeSaga")
    @Parameters({
            @Parameter(name = "productCode",description = "商品code",required = true),
            @Parameter(name = "count",description = "数量",required = true)
    })
    void reduceCountByCodeSaga(@RequestParam("productCode") String productCode, @RequestParam("count") Integer count) throws BizException;

    @Operation(summary = "补偿-根据商品code扣减数量-Saga")
    @PostMapping("/compensateReduceCountByCodeSaga")
    @Parameters({
            @Parameter(name = "productCode",description = "商品code",required = true),
            @Parameter(name = "count",description = "数量",required = true)
    })
    void compensateReduceCountByCodeSaga(@RequestParam("productCode") String productCode, @RequestParam("count") Integer count) throws BizException;

ProductFeignClientFallback 添加方法如下。

@Override
    public void reduceCountByCodeSaga(String productCode, Integer count) throws BizException {
        throw ExceptionFactory.bizException("reduceCountByCodeSaga-扣减库存失败,方法:reduceCountByCode,参数:productCode="+productCode+",count="+count);
    }

    @Override
    public void compensateReduceCountByCodeSaga(String productCode, Integer count) throws BizException {
        throw ExceptionFactory.bizException("compensateReduceCountByCodeSaga-补偿扣减库存失败,方法:reduceCountByCode,参数:productCode="+productCode+",count="+count);
    }
6.7.1.2.4. mall-product-center-adapter 工程

给 ProductClientController 添加方法如下。

@Override
    public void reduceCountByCodeSaga(String productCode, Integer count) throws BizException {
        this.productExecutor.reduceProductCountByCodeSaga(productCode,count);
    }

    @Override
    public void compensateReduceCountByCodeSaga(String productCode, Integer count) throws BizException {
        this.productExecutor.compensateReduceProductCountByCodeSaga(productCode,count);
    }
6.7.1.2.5. mall-product-center-app工程

给 ProductExecutor 添加方法如下。

@Component
public class ProductExecutor {

    @Resource
    private ProductService productService;
    @Resource
    private ProductTccAction productTccAction;
    @Resource
    private ProductSagaAction productSagaAction;

    /**
     * 省略其他方法
     */

    /**
     *  根据商品code扣减库存-Saga
     */
    public void reduceProductCountByCodeSaga(String productCode, Integer count) throws BizException {
        this.productSagaAction.reduceCountByCode(productCode,count);
    }

    /**
     * 补偿-根据商品code扣减库存-Saga
     */
    public void compensateReduceProductCountByCodeSaga(String productCode, Integer count) throws BizException {
        this.productSagaAction.compensateReduceCountByCode(productCode,count);
    }

}
6.7.1.3. 订单模块创建订单方法实现
6.7.1.3.1. mall-order-center-domain工程

在 org.example.order.domain.action 目录下添加 OrderSagaAction 类。代码如下。

public interface OrderSagaAction {

    /**
     *  创建订单
     */
    boolean createOrder(OrderReqDTO orderReqDTO,Long id);

    /**
     * 创建订单-方法补偿
     */
    boolean compensateCreateOrder(Long id);
}
6.7.1.3.2. mall-order-center-infra工程

在 org.example.order.infra.impl 目录下添加 OrderSagaActionImpl 类。代码如下。

@Slf4j
@Service("orderSagaAction")
public class OrderSagaActionImpl implements OrderSagaAction {

    @Resource
    private OrderService orderService;

    @Override
    public boolean createOrder(OrderReqDTO orderReqDTO, Long id) {
        log.info("createOrder--------orderReqDTO={},id={}", JSON.toJSONString(orderReqDTO),id);
        if(Objects.isNull(orderReqDTO)){
            throw ExceptionFactory.bizException("订单参数为空!");
        }
        this.orderService.add(orderReqDTO,id);
        return true;
    }

    @Override
    public boolean compensateCreateOrder(Long id) {
        log.info("createOrderCancel--------id={}",id);
        this.orderService.removeById(id);
        return true;
    }
}
6.7.1.3.3. mall-order-center-adapter 工程

在 OrderController 控制器中,将 createOrder 接口调用创建订单的方法改为调用 createOrderSaga,。

@Operation(summary = "创建订单")
    @PostMapping("/createOrder")
    public ResponseResult<String> createOrder(@RequestBody OrderReqDTO orderReqDTO){
        log.info("createOrder order, orderReqDTO:{}",orderReqDTO);
        this.orderExecutor.createOrderSaga(orderReqDTO);
        return ResponseResult.ok("createOrder Order succeed");
    }
6.7.1.3.4. mall-order-center-app工程

在 org.example.order.app 包目录下 ,创建 config 包目录,在 config 包目录下创建名称为 StatemachineEngineConfig 的配置类,代码如下。

@Configuration
public class StatemachineEngineConfig {

    @Value("${seata.tx-service-group}")
    private String txServiceGroup;
    @Resource
    private DataSource dataSource;


    @Bean("sagaThreadExecutor")
    ThreadPoolExecutor sagaThreadExecutor(){
        ThreadPoolExecutorFactoryBean threadPoolExecutorFactoryBean = new ThreadPoolExecutorFactoryBean();
        threadPoolExecutorFactoryBean.setThreadNamePrefix("SAGA_ASYNC_EXE_");
        threadPoolExecutorFactoryBean.setCorePoolSize(1);
        threadPoolExecutorFactoryBean.setMaxPoolSize(20);
        threadPoolExecutorFactoryBean.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return new ThreadPoolExecutor(1, 20, 3, TimeUnit.SECONDS, new LinkedBlockingDeque<>(), threadPoolExecutorFactoryBean, new ThreadPoolExecutor.CallerRunsPolicy());
    }

    @Bean("dbStateMachineConfig")
    DbStateMachineConfig dbStateMachineConfig(){
        DbStateMachineConfig dbStateMachineConfig = new DbStateMachineConfig();
        dbStateMachineConfig.setDataSource(dataSource);
        dbStateMachineConfig.setResources(new String[]{"saga/statelang/*.json"});
        dbStateMachineConfig.setEnableAsync(true);
        dbStateMachineConfig.setThreadPoolExecutor(sagaThreadExecutor());
        dbStateMachineConfig.setApplicationId(BaseConstant.Domain.PRODUCT);
        dbStateMachineConfig.setTxServiceGroup(txServiceGroup);
        dbStateMachineConfig.setSagaBranchRegisterEnable(false);
        dbStateMachineConfig.setSagaJsonParser("jackson");
        dbStateMachineConfig.setSagaRetryPersistModeUpdate(false);
        dbStateMachineConfig.setSagaCompensatePersistModeUpdate(false);
        return dbStateMachineConfig;
    }

    @Bean("stateMachineEngine")
    ProcessCtrlStateMachineEngine stateMachineEngine(){
        ProcessCtrlStateMachineEngine processCtrlStateMachineEngine = new ProcessCtrlStateMachineEngine();
        processCtrlStateMachineEngine.setStateMachineConfig(dbStateMachineConfig());
        return processCtrlStateMachineEngine;
    }

    @Bean
    StateMachineEngineHolder stateMachineEngineHolder(){
        StateMachineEngineHolder stateMachineEngineHolder = new StateMachineEngineHolder();
        stateMachineEngineHolder.setStateMachineEngine(stateMachineEngine());
        return stateMachineEngineHolder;
    }
}

给 OrderExecutor 添加调用状态机创建订单的方法,代码如下。

@Component
public class OrderExecutor {

    @Resource
    private OrderService orderService;
    @Resource
    private OrderTccAction orderTccAction;
    @Resource
    private ProductFeignClient productFeignClient;
    @Resource
    private AccountFeignClient accountFeignClient;

    /**
     *  省略其他方法
     */

    /**
     *  创建订单-Saga
     */
    @GlobalTransactional
    public void createOrderSaga(OrderReqDTO orderReqDTO) throws BaseException {

        StateMachineEngine stateMachineEngine = StateMachineEngineHolder.getStateMachineEngine();
        Long id = IdWorker.getId();
        Map<String, Object> startParams = new HashMap<>(6);
        startParams.put("id", id);
        startParams.put("productCode", orderReqDTO.getProductCode());
        startParams.put("count", orderReqDTO.getCount());
        startParams.put("accountCode", orderReqDTO.getAccountCode());
        startParams.put("amount", orderReqDTO.getAmount());
        startParams.put("orderReqDTO", orderReqDTO);
        StateMachineInstance inst = stateMachineEngine.start("createOrderFlow",null,startParams);
        log.info("saga transaction commit succeed. XID: {}",inst.getId());
        if(!ExecutionStatus.SU.equals(inst.getStatus())){
            throw ExceptionFactory.bizException("saga transaction execute failed. XID: " + inst.getId());
        }
    }
}

这里涉及到在订单服务远程调用账户服务、商品服务,由于 AccountFeignClient、ProductFeignClient 是接口,如果将它们作为 Bean,直接配置到状态机的 ServiceName 中,Saga 将会找不到 AccountFeignClient、ProductFeignClient 对应的 Bean。因此,我们需要对 AccountFeignClient、ProductFeignClient 中参与 Saga 事务的方法进行进行间接调用,处理方法如下。

添加 org.example.order.app.action 目录,创建 AccountSagaAction、ProductSagaAction 接口,

代码如下。

public interface AccountSagaAction {


    /**
     *  根据账户code扣减余额
     */
    boolean reduceAmountByCode(String accountCode, BigDecimal amount);

    /**
     *  根据账户code扣减余额-方法补偿
     */
    boolean compensateReduceAmountByCode(String accountCode, BigDecimal amount);
}


public interface ProductSagaAction {

    /**
     *  根据商品code扣减库存
     */
    boolean reduceCountByCode(String productCode, Integer count);

    /**
     *  根据商品code扣减库存-方法补偿
     */
    boolean compensateReduceCountByCode(String productCode, Integer count);
}

添加 org.example.order.app.action.impl 目录,分别创建 AccountSagaAction、ProductSagaAction 接口的实现类 AccountSagaActionImpl、ProductSagaActionImpl,代码如下。

@Slf4j
@Service("accountSagaAction")
public class AccountSagaActionImpl implements AccountSagaAction {

    @Resource
    private AccountFeignClient accountFeignClient;

    @Override
    public boolean reduceAmountByCode(String accountCode, BigDecimal amount) {
        log.info("reduceAmountByCode--------accountCode={},amount={}", accountCode,amount);
        this.accountFeignClient.reduceAmountByCode(accountCode,amount);
        return true;
    }

    @Override
    public boolean compensateReduceAmountByCode(String accountCode, BigDecimal amount) {
        log.info("compensateReduceAmountByCode--------accountCode={},amount={}", accountCode,amount);
        this.accountFeignClient.compensateReduceAmountByCodeSaga(accountCode,amount);
        return true;
    }
}


@Slf4j
@Service("productSagaAction")
public class ProductSagaActionImpl implements ProductSagaAction {

    @Resource
    private ProductFeignClient productFeignClient;


    @Override
    public boolean reduceCountByCode(String productCode, Integer count) {
        log.info("reduceCountByCode--------productCode={},count={}", productCode,count);
        this.productFeignClient.reduceCountByCodeSaga(productCode,count);
        return true;
    }

    @Override
    public boolean compensateReduceCountByCode(String productCode, Integer count) {
        log.info("reduceCountByCodeCancel--------productCode={},count={}", productCode,count);
        this.productFeignClient.compensateReduceCountByCodeSaga(productCode,count);
        return true;
    }
}

这样,就可以将 accountSagaAction、productSagaAction 作为 Bean 的名称,配置到状态的 ServiceName 中了。

6.7.2. 状态机设计

在 mall-order-center-start 工程的 resources 目录下,创建“saga\statelang”目录, 在“resources\saga\statelang”目录下,创建名称为 saga-created-order-flow.json 的状态机文件,内容如下。

{
  "nodes": [
    {
      "type": "node",
      "size": "180*48",
      "shape": "flow-capsule",
      "color": "#722ED1",
      "label": "compensateReduceAmount",
      "stateId": "reduce-amount-compensation",
      "stateType": "Compensation",
      "stateProps": {
        "ServiceName": "accountSagaAction",
        "ServiceMethod": "compensateReduceAmountByCode",
        "Input": [
          "$.[accountCode]",
          "$.[amount]"
        ],
        "Output": {
          "compensateReduceAmountResult": "$.#root"
        },
        "Status": {
          "#root == true": "SU",
          "#root == false": "FA",
          "$Exception{java.lang.Throwable}": "UN"
        },
        "Retry": []
      },
      "x": 118.125,
      "y": -265.5,
      "id": "77883822",
      "index": 17
    },
    {
      "type": "node",
      "size": "39*39",
      "shape": "flow-circle",
      "color": "red",
      "label": "Catch",
      "stateId": "create-order-catch",
      "stateType": "Catch",
      "x": 379.125,
      "y": 68.5,
      "id": "99285853",
      "index": 11
    },
    {
      "type": "node",
      "size": "72*72",
      "shape": "flow-circle",
      "color": "#FA8C16",
      "label": "Start",
      "stateId": "create-order-start",
      "stateType": "Start",
      "stateProps": {
        "StateMachine": {
          "Name": "createOrderFlow",
          "Comment": "create order flow,reduce count,reduce amount,create order",
          "Version": "0.0.1"
        }
      },
      "x": 323.625,
      "y": -565,
      "id": "be648d4f",
      "index": 0
    },
    {
      "type": "node",
      "size": "110*48",
      "shape": "flow-rect",
      "color": "#1890FF",
      "label": "reduceCount",
      "stateId": "reduce-count-service-task",
      "stateType": "ServiceTask",
      "stateProps": {
        "ServiceName": "productSagaAction",
        "ServiceMethod": "reduceCountByCode",
        "Input": [
          "$.[productCode]",
          "$.[count]"
        ],
        "Output": {
          "reduceCountResult": "$.#root"
        },
        "Status": {
          "#root == true": "SU",
          "#root == false": "FA",
          "$Exception{java.lang.Throwable}": "UN"
        },
        "Retry": []
      },
      "x": 323.625,
      "y": -447.5,
      "id": "c9a51329",
      "index": 1
    },
    {
      "type": "node",
      "size": "39*39",
      "shape": "flow-circle",
      "color": "red",
      "label": "Catch",
      "stateId": "reduce-amount-catch",
      "stateType": "Catch",
      "x": 380.125,
      "y": -182.5,
      "id": "167e3e70",
      "index": 12
    },
    {
      "type": "node",
      "size": "110*48",
      "shape": "flow-rect",
      "color": "#1890FF",
      "label": "reduceAmount",
      "stateId": "reduce-amount-service-task",
      "stateType": "ServiceTask",
      "stateProps": {
        "ServiceName": "accountSagaAction",
        "ServiceMethod": "reduceAmountByCode",
        "Input": [
          "$.[accountCode]",
          "$.[amount]"
        ],
        "Output": {
          "reduceAmountResult": "$.#root"
        },
        "Status": {
          "#root == true": "SU",
          "#root == false": "FA",
          "$Exception{java.lang.Throwable}": "UN"
        },
        "Retry": []
      },
      "x": 324.625,
      "y": -195,
      "id": "bbd4a3d6",
      "index": 18
    },
    {
      "type": "node",
      "size": "80*72",
      "shape": "flow-rhombus",
      "color": "#13C2C2",
      "label": "Choice",
      "stateId": "reduce-amount-choice",
      "stateType": "Choice",
      "x": 324.625,
      "y": -84,
      "id": "202e18cb",
      "index": 19
    },
    {
      "type": "node",
      "size": "110*48",
      "shape": "flow-rect",
      "color": "#1890FF",
      "label": "createOrder",
      "stateId": "create-order-service-task",
      "stateType": "ServiceTask",
      "stateProps": {
        "ServiceName": "orderSagaAction",
        "ServiceMethod": "createOrder",
        "ParameterTypes": [
          "org.example.order.dto.req.OrderReqDTO",
          "java.lang.Long"
        ],
        "Input": [
          {
            "accountCode": "$.[orderReqDTO].accountCode",
            "productCode": "$.[orderReqDTO].productCode",
            "count": "$.[orderReqDTO].count",
            "amount": "$.[orderReqDTO].amount"
          },
          "$.[id]"
        ],
        "Output": {
          "createOrderResult": "$.#root"
        },
        "Status": {
          "#root == true": "SU",
          "#root == false": "FA",
          "$Exception{java.lang.Throwable}": "UN"
        },
        "Retry": []
      },
      "x": 324.625,
      "y": 48,
      "id": "144251b0",
      "index": 20
    },
    {
      "type": "node",
      "size": "72*72",
      "shape": "flow-circle",
      "color": "#05A465",
      "label": "Succeed",
      "stateId": "succeed",
      "stateType": "Succeed",
      "x": 324.625,
      "y": 173,
      "id": "efca467d",
      "index": 21
    },
    {
      "type": "node",
      "size": "80*72",
      "shape": "flow-rhombus",
      "color": "#13C2C2",
      "label": "Choice",
      "stateId": "reduce-count-choice",
      "stateType": "Choice",
      "x": 323.625,
      "y": -334,
      "id": "efbc1565",
      "index": 22
    },
    {
      "type": "node",
      "size": "180*48",
      "shape": "flow-capsule",
      "color": "#722ED1",
      "label": "compensateReduceCount",
      "stateId": "reduce-count-compensation",
      "stateType": "Compensation",
      "stateProps": {
        "ServiceName": "productSagaAction",
        "ServiceMethod": "compensateReduceCountByCode",
        "Input": [
          "$.[productCode]",
          "$.[count]"
        ],
        "Output": {
          "compensateReduceCountResult": "$.#root"
        },
        "Status": {
          "#root == true": "SU",
          "#root == false": "FA",
          "$Exception{java.lang.Throwable}": "UN"
        },
        "Retry": []
      },
      "x": 136.125,
      "y": -496.5,
      "id": "91f70287",
      "index": 24
    },
    {
      "type": "node",
      "size": "72*72",
      "shape": "flow-circle",
      "color": "red",
      "label": "Fail",
      "stateId": "fail",
      "stateType": "Fail",
      "stateProps": {
        "ErrorCode": "PURCHASE_FAILED",
        "Message": "purchase failed"
      },
      "x": 630.625,
      "y": -334,
      "id": "765ed40d",
      "index": 25
    },
    {
      "type": "node",
      "size": "110*48",
      "shape": "flow-capsule",
      "color": "red",
      "label": "Compensation\nTrigger",
      "stateId": "compensation-trigger",
      "stateType": "CompensationTrigger",
      "x": 630.625,
      "y": -182.5,
      "id": "169d41c5",
      "index": 26
    },
    {
      "type": "node",
      "size": "180*48",
      "shape": "flow-capsule",
      "color": "#722ED1",
      "label": "compensateCreateOrder",
      "stateId": "create-order-compensation",
      "stateType": "Compensation",
      "stateProps": {
        "ServiceName": "orderSagaAction",
        "ServiceMethod": "compensateCreateOrder",
        "Input": [
          "$.[id]"
        ],
        "Output": {
          "compensateCreateOrderResult": "$.#root"
        },
        "Status": {
          "#root == true": "SU",
          "#root == false": "FA",
          "$Exception{java.lang.Throwable}": "UN"
        },
        "Retry": []
      },
      "x": 108.125,
      "y": -36.5,
      "id": "bb32be7b"
    }
  ],
  "edges": [
    {
      "source": "bbd4a3d6",
      "sourceAnchor": 3,
      "target": "77883822",
      "targetAnchor": 2,
      "id": "ec7ad3aa",
      "shape": "flow-polyline-round",
      "style": {
        "lineDash": "4",
        "endArrow": false
      },
      "type": "Compensation",
      "index": 5
    },
    {
      "source": "c9a51329",
      "sourceAnchor": 3,
      "target": "91f70287",
      "targetAnchor": 2,
      "id": "02716f5c",
      "shape": "flow-polyline-round",
      "style": {
        "lineDash": "4",
        "endArrow": false
      },
      "type": "Compensation",
      "index": 6
    },
    {
      "source": "167e3e70",
      "sourceAnchor": 1,
      "target": "169d41c5",
      "targetAnchor": 3,
      "id": "0a2bd221",
      "shape": "flow-polyline-round",
      "stateProps": {
        "Exceptions": [
          "java.lang.Throwable"
        ]
      },
      "index": 9
    },
    {
      "source": "efbc1565",
      "sourceAnchor": 2,
      "target": "bbd4a3d6",
      "targetAnchor": 0,
      "id": "4f1c37ad",
      "shape": "flow-polyline-round",
      "stateProps": {
        "Expression": "[reduceCountResult] == true",
        "Default": false
      },
      "label": "Choice",
      "index": 13
    },
    {
      "source": "bbd4a3d6",
      "sourceAnchor": 2,
      "target": "202e18cb",
      "targetAnchor": 0,
      "id": "cd1dd5f5",
      "shape": "flow-polyline-round",
      "index": 14
    },
    {
      "source": "efbc1565",
      "sourceAnchor": 1,
      "target": "765ed40d",
      "targetAnchor": 3,
      "id": "8ac9c9e9",
      "shape": "flow-polyline-round",
      "stateProps": {
        "Expression": "[reduceCountResult] == false",
        "Default": false
      },
      "index": 16,
      "label": ""
    },
    {
      "source": "be648d4f",
      "sourceAnchor": 2,
      "target": "c9a51329",
      "targetAnchor": 0,
      "id": "1bc85138",
      "shape": "flow-polyline-round",
      "label": "",
      "index": 23
    },
    {
      "source": "202e18cb",
      "sourceAnchor": 2,
      "target": "144251b0",
      "targetAnchor": 0,
      "id": "3b92ffb2",
      "shape": "flow-polyline-round",
      "stateProps": {
        "Expression": "[reduceAmountResult]==true",
        "Default": false
      },
      "label": "Choice"
    },
    {
      "source": "144251b0",
      "sourceAnchor": 2,
      "target": "efca467d",
      "targetAnchor": 0,
      "id": "2c0e194a",
      "shape": "flow-polyline-round"
    },
    {
      "source": "144251b0",
      "sourceAnchor": 3,
      "target": "bb32be7b",
      "targetAnchor": 2,
      "id": "ed21c808",
      "shape": "flow-polyline-round",
      "style": {
        "lineDash": "4",
        "endArrow": false
      },
      "type": "Compensation"
    },
    {
      "source": "169d41c5",
      "sourceAnchor": 0,
      "target": "765ed40d",
      "targetAnchor": 2,
      "id": "2888277a",
      "shape": "flow-polyline-round"
    },
    {
      "source": "99285853",
      "sourceAnchor": 1,
      "target": "169d41c5",
      "targetAnchor": 2,
      "id": "b211ada4",
      "shape": "flow-polyline-round",
      "stateProps": {
        "Exceptions": [
          "java.lang.Throwable"
        ]
      }
    },
    {
      "source": "202e18cb",
      "sourceAnchor": 1,
      "target": "169d41c5",
      "targetAnchor": 2,
      "id": "b9c1fe95",
      "shape": "flow-polyline-round",
      "stateProps": {
        "Expression": "[reduceAmountResult]==false",
        "Default": false
      },
      "label": ""
    },
    {
      "source": "c9a51329",
      "sourceAnchor": 2,
      "target": "efbc1565",
      "targetAnchor": 0,
      "id": "4912ec0e",
      "shape": "flow-polyline-round"
    }
  ]
}

saga-created-order-flow.json 文件对应的状态机效果图如下。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Cloud_35

上面的状态机可以使用 官网地址 提供的界面来设计。

6.7.3. 建表 sql 初始化

Saga 模式,需要用到 seata_state_machine_def、seata_state_machine_inst、seata_state_inst 三张表。需要分别在账户模块、商品模块、订单模块的数据库中添加这三张表。下面是初始化三张表的 sql 脚本。

-- -------------------------------- The script used for sage  --------------------------------


CREATE TABLE IF NOT EXISTS `seata_state_machine_def`
(
    `id`               VARCHAR(32)  NOT NULL COMMENT 'id',
    `name`             VARCHAR(128) NOT NULL COMMENT 'name',
    `tenant_id`        VARCHAR(32)  NOT NULL COMMENT 'tenant id',
    `app_name`         VARCHAR(32)  NOT NULL COMMENT 'application name',
    `type`             VARCHAR(20)  COMMENT 'state language type',
    `comment_`         VARCHAR(255) COMMENT 'comment',
    `ver`              VARCHAR(16)  NOT NULL COMMENT 'version',
    `gmt_create`       DATETIME(3)  NOT NULL COMMENT 'create time',
    `status`           VARCHAR(2)   NOT NULL COMMENT 'status(AC:active|IN:inactive)',
    `content`          TEXT COMMENT 'content',
    `recover_strategy` VARCHAR(16) COMMENT 'transaction recover strategy(compensate|retry)',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

CREATE TABLE IF NOT EXISTS `seata_state_machine_inst`
(
    `id`                  VARCHAR(128)            NOT NULL COMMENT 'id',
    `machine_id`          VARCHAR(32)             NOT NULL COMMENT 'state machine definition id',
    `tenant_id`           VARCHAR(32)             NOT NULL COMMENT 'tenant id',
    `parent_id`           VARCHAR(128) COMMENT 'parent id',
    `gmt_started`         DATETIME(3)             NOT NULL COMMENT 'start time',
    `business_key`        VARCHAR(48) COMMENT 'business key',
    `start_params`        TEXT COMMENT 'start parameters',
    `gmt_end`             DATETIME(3) COMMENT 'end time',
    `excep`               BLOB COMMENT 'exception',
    `end_params`          TEXT COMMENT 'end parameters',
    `status`              VARCHAR(2) COMMENT 'status(SU succeed|FA failed|UN unknown|SK skipped|RU running)',
    `compensation_status` VARCHAR(2) COMMENT 'compensation status(SU succeed|FA failed|UN unknown|SK skipped|RU running)',
    `is_running`          TINYINT(1) COMMENT 'is running(0 no|1 yes)',
    `gmt_updated`         DATETIME(3) NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `unikey_buz_tenant` (`business_key`, `tenant_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

CREATE TABLE IF NOT EXISTS `seata_state_inst`
(
    `id`                       VARCHAR(48)  NOT NULL COMMENT 'id',
    `machine_inst_id`          VARCHAR(128) NOT NULL COMMENT 'state machine instance id',
    `name`                     VARCHAR(128) NOT NULL COMMENT 'state name',
    `type`                     VARCHAR(20)  COMMENT 'state type',
    `service_name`             VARCHAR(128) COMMENT 'service name',
    `service_method`           VARCHAR(128) COMMENT 'method name',
    `service_type`             VARCHAR(16) COMMENT 'service type',
    `business_key`             VARCHAR(48) COMMENT 'business key',
    `state_id_compensated_for` VARCHAR(50) COMMENT 'state compensated for',
    `state_id_retried_for`     VARCHAR(50) COMMENT 'state retried for',
    `gmt_started`              DATETIME(3)  NOT NULL COMMENT 'start time',
    `is_for_update`            TINYINT(1) COMMENT 'is service for update',
    `input_params`             TEXT COMMENT 'input parameters',
    `output_params`            TEXT COMMENT 'output parameters',
    `status`                   VARCHAR(2)   NOT NULL COMMENT 'status(SU succeed|FA failed|UN unknown|SK skipped|RU running)',
    `excep`                    BLOB COMMENT 'exception',
    `gmt_updated`              DATETIME(3) COMMENT 'update time',
    `gmt_end`                  DATETIME(3) COMMENT 'end time',
    PRIMARY KEY (`id`, `machine_inst_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

6.7.4. 功能测试

6.7.4.1. 正常流程测试

启动账户模块、商品模块、订单模块的服务。测试前,mall_account 表中,account_code = '1000002' 的记录,账户余额为 1000.00;mall_product 表中,product_code='100001' 的记录,库存数量为 10;mall_order 表中记录为空。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Cloud Alibaba_36

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_分布式事务-Seata_37

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_COLA_38

前端输入以下参数发起请求,则返回成功。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_COLA_39

此时,mall_account 表中,account_code = '1000002' 的记录,账户余额为 980.00;

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Cloud_40

        mall_product 表中,product_code='100001' 的记录,库存数量为 9;

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_COLA_41

mall_order 表中,则插入了一条 account_code = '1000002'、product_code='100001' 的记录。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Cloud Alibaba_42

6.7.4.2. 异常流程测试

在 org.example.order.infra.impl.OrderSagaActionImpl#createOrder 方法中,加入“int a = 1/0;”的异常代码。

前端输入以下参数发起请求,则出现结果报错。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Cloud_43

报错的主要日志如下。

INFO 13892 --- [nio-7050-exec-1] o.e.o.infra.impl.OrderSagaActionImpl     : createOrder--------orderReqDTO={"accountCode":"1000002","amount":20,"count":1,"productCode":"100001"},id=1700858392476233729
 ERROR 13892 --- [nio-7050-exec-1] i.s.s.e.p.h.ServiceTaskStateHandler      : <<<<<<<<<<<<<<<<<<<<<< State[create-order-service-task], ServiceName[orderSagaAction], Method[createOrder] Execute failed.

java.lang.ArithmeticException: / by zero
	at org.example.order.infra.impl.OrderSagaActionImpl.createOrder(OrderSagaActionImpl.java:34) ~[classes/:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]

此时,mall_account 表中,account_code = '1000002' 的记录,账户余额仍为 980.00;

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Cloud Alibaba_44

mall_product 表中,product_code='100001' 的记录,库存数量仍为 9;

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Boot3_45

mall_order 表中,还是只有一条记录。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_COLA_46

6.8. XA 模式

6.8.1. 代码实现

XA模式需配置数据源代理,分别给账户模块、商品模块、订单模块添加数据源代理配置。以订单模块为例。在 mall-order-center-infra 工程中新建 org.example.order.infra.config 目录,在该目录下,创建名称为 OrderXADataSourceConfiguration 的配置类。内容如下。

@Configuration
public class ProductXADataSourceConfiguration {

    @Value("${spring.datasource.url}")
    private String datasourceUrl;
    @Value("${spring.datasource.username}")
    private String datasourceUsername;
    @Value("${spring.datasource.password}")
    private String datasourcePassword;


    @Bean("dataSourceProxy")
    public DataSource dataSource() {

        HikariDataSource hikariDataSource = new HikariDataSource();
        hikariDataSource.setJdbcUrl(datasourceUrl);
        hikariDataSource.setUsername(datasourceUsername);
        hikariDataSource.setPassword(datasourcePassword);
        // DataSourceProxy for AT mode
        //return new DataSourceProxy(hikariDataSource);

        // DataSourceProxyXA for XA mode
        return new DataSourceProxyXA(hikariDataSource);
    }

    @Bean("jdbcTemplate")
    public JdbcTemplate jdbcTemplate(DataSource dataSourceProxy) {
        return new JdbcTemplate(dataSourceProxy);
    }

    @Bean
    public PlatformTransactionManager txManager(DataSource dataSourceProxy) {
        return new DataSourceTransactionManager(dataSourceProxy);
    }

}

在 seata-client.yaml 中,设置数据源代理模式为 XA。如下所示。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Boot3_47

6.8.2. 功能测试

将 OrderController 创建订单的方法,改回如下代码。

@Operation(summary = "创建订单")
    @PostMapping("/createOrder")
    public ResponseResult<String> createOrder(@RequestBody OrderReqDTO orderReqDTO){
        log.info("createOrder order, orderReqDTO:{}",orderReqDTO);
        this.orderExecutor.createOrder(orderReqDTO);
        return ResponseResult.ok("createOrder Order succeed");
    }

org.example.order.app.executor.OrderExecutor#createOrder 方法代码如下。

/**
     *  创建订单
     */
    @GlobalTransactional
    public void createOrder(OrderReqDTO orderReqDTO) throws BaseException {
        this.addOrder(orderReqDTO);
        this.productFeignClient.reduceCountByCode(orderReqDTO.getProductCode(),orderReqDTO.getCount());
        this.accountFeignClient.reduceAmountByCode(orderReqDTO.getAccountCode(),orderReqDTO.getAmount());
        int a = 1/0;
    }

启动账户模块、商品模块、订单模块的服务。测试前,mall_account 表中,account_code = '1000002' 的记录,账户余额为 1000.00;mall_product 表中,product_code='100001' 的记录,库存数量为 10;mall_order 表中记录为空。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Cloud Alibaba_48

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_COLA_49

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_COLA_50

前端输入以下参数发起请求,则出现结果报错。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Cloud_51

报错日志如下所示。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_分布式事务-Seata_52

再查看 mall_account、mall_product、mall_order 三表的数据,均无变化。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_分布式事务-Seata_53

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Cloud Alibaba_54

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Cloud Alibaba_55

验证是否是 XA 模式的事务。

在 org.example.order.app.executor.OrderExecutor#createOrder 方法上,打上如下断点,前端再次发起请求。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Cloud_56

查看 branch_table 表的数据如下。

基于 COLA 架构的 Spring Cloud Alibaba(五)整合 Seata_Spring Cloud Alibaba_57

可以看到,事务分支注册类型为 XA。

7. 总结

本篇先介绍了 Seata Server 的搭建,及 Seata 客户端的整合。接着提出了一个简单的下单需求场景。然后对该下单的需求场景,分别使用 AT、TCC、Saga、XA 四种模式进行实现并测试。对于 AT、TCC、Saga、XA 四种模式。AT 模式实现起来,是四种模式中最简单的模式,也是 Seata 默认的模式。XA 模式与 AT 模式非常相似,只需对数据源做少量代码的配置即可像 AT 模式一样使用。TCC 模式需要业务提供 prepare 、commit 和 rollback 三个的方法,代码入侵较多,但 TCC 模式在几种模式中效率较高,因为TCC 模式在 try 阶段就提交本地事务了。Saga 模式应该是四种模式中使用起来最为复杂,门槛最高的模式了,Saga 模式适合使用在业务流程长、业务流程多,或者参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口的场景。以上四种模式,可以根据实际场景,选择合适额模式进行使用。


基础篇项目代码:链接地址