手把手教你Spring Cloud集成Seata TCC模式

1,922 阅读11分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第14天,点击查看活动详情

前言

在博客Spring Cloud集成分布式事务框架Seata 1.5.2中,我们已经集成了Seata AT模式,虽然AT模式可以覆盖大部分分布式事务需求,但是针对于一些追求高性能的业务场景,我们还是需要选择TCC模式;

因为TCC的资源预留概念降低了锁的粒度,在分布式事务未完成前并不会阻塞同业务下的其他分布式事务的执行;但是有一点不好的就是:TCC模式对于业务侵入性比较大,整个分布式事务的资源准备提交回滚逻辑全部需要开发人员自己完成,开发工作量比AT模式大出两三倍;

为了在将来的工作中能够顺利地使用TCC模式来作为高性能业务的分布式事务解决方案,我们下面就开始手把手教大家如何在Spring Cloud中集成Seata TCC模式。

业务场景

image-20221011180312260.png

同样还是购物车下单的业务场景:

1.用户通过业务入口提交下单数据;

2.先计算订单总金额并调用RPC进行预扣款;

  • 2.1:预扣款成功才能创建订单;
  • 2.2:预扣款失败,TM发起回滚,Account服务调用回滚逻辑,分布式事务回滚结束;

3.预扣款成功,那么再发起RPC创建订单;

  • 3.1:Order服务接收到创建订单的请求,先RPC执行预扣库存的操作;
  • 3.2:Storage服务预扣库存成功,Order服务执行预创建订单操作;
  • 3.3:Storage服务预扣库存失败,导致订单创建失败,下单业务抛出异常,TM发起回滚,Account服务与Storage服务调用回滚逻辑,分布式事务回滚结束;
  • 3.4:Order服务执行预创建订单成功,下单业务执行完毕,TM发起提交,所有RM调用提交逻辑,分布式事务成功;
  • 3.5:Order服务执行预创建订单失败,下单业务抛出异常,TM发起回滚,所有RM调用回滚逻辑,分布式事务回滚结束;

数据表创建

注意:TCC模式不需要创建undolog表

账户表:wallet_tcc_tbl

-- ----------------------------
-- Table structure for wallet_tcc_tbl
-- ----------------------------
DROP TABLE IF EXISTS `wallet_tcc_tbl`;
CREATE TABLE `wallet_tcc_tbl` (
  `id` int NOT NULL COMMENT '主键ID',
  `user_id` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '用户ID',
  `money` int NOT NULL COMMENT '账户金额',
  `pre_money` int NOT NULL COMMENT '预扣金额',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
INSERT INTO `wallet_tcc_tbl` (`id`, `user_id`, `money`, `pre_money`) VALUES (1, '123456', 1000000, 0);

订单表:order_tcc_tbl

-- ----------------------------
-- Table structure for order_tcc_tbl
-- ----------------------------
DROP TABLE IF EXISTS `order_tcc_tbl`;
CREATE TABLE `order_tcc_tbl` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `user_id` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '用户ID',
  `commodity_code` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '商品编码',
  `count` int NOT NULL COMMENT '商品数量',
  `unit_price` int NOT NULL COMMENT '商品单价',
  `status` varchar(8) COLLATE utf8mb4_bin NOT NULL COMMENT '订单状态',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

库存表:stock_tcc_tbl

-- ----------------------------
-- Table structure for stock_tcc_tbl
-- ----------------------------
DROP TABLE IF EXISTS `stock_tcc_tbl`;
CREATE TABLE `stock_tcc_tbl` (
  `id` int NOT NULL COMMENT '主键ID',
  `commodity_code` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '商品编码',
  `count` int NOT NULL COMMENT '商品总数',
  `pre_deduct_count` int NOT NULL COMMENT '预扣数量',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
INSERT INTO `stock_tcc_tbl` (`id`, `commodity_code`, `count`, `pre_deduct_count`) VALUES (1, 'CC-54321', 10000, 0);

创建TCC Action

在各服务表创建完毕后,我们开始编写核心的TCC Action,用以完成我们的TryCommitCancal逻辑。

Account服务

  • 先构建接口

    import io.seata.rm.tcc.api.BusinessActionContext;
    import io.seata.rm.tcc.api.BusinessActionContextParameter;
    import io.seata.rm.tcc.api.LocalTCC;
    import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
    ​
    /**
     * @author zouwei
     * @className IWalletTccAction
     * @date: 2022/10/10 20:50
     * @description:
     */
    @LocalTCC
    public interface IWalletTccAction {
    ​
      /**
       * 预扣款
       *
       * @param businessActionContext
       * @param userId
       * @param amount
       * @return
       */
      @TwoPhaseBusinessAction(name = "prepareDeductMoney", commitMethod = "commitDeductMoney", rollbackMethod = "rollbackDeductMoney")
      boolean prepareDeductMoney(BusinessActionContext businessActionContext,
                                 @BusinessActionContextParameter(paramName = "userId") String userId,
                                 @BusinessActionContextParameter(paramName = "amount") Long amount);
    ​
      /**
       * 提交扣款
       *
       * @param businessActionContext
       * @return
       */
      boolean commitDeductMoney(BusinessActionContext businessActionContext);
    ​
      /**
       * 回滚扣款
       *
       * @param businessActionContext
       * @return
       */
      boolean rollbackDeductMoney(BusinessActionContext businessActionContext);
    }
    

    1.@LocalTCC注解表示该接口是一个TCC Action接口,需要Seata处理;

    2.@TwoPhaseBusinessAction注解标注了提交和回滚方法,以便Seata根据预处理结果来决定时调用提交方法还是回滚方法

  • 实现扣款服务逻辑

    import com.example.awesomeaccount.dao.mapper.WalletTccEnhanceMapper;
    import com.example.awesomeaccount.tcc.IWalletTccAction;
    import com.example.awesomeaccount.tcc.TccActionResultWrap;
    import io.seata.rm.tcc.api.BusinessActionContext;
    import org.springframework.stereotype.Component;
    import org.springframework.transaction.annotation.Transactional;
    ​
    import javax.annotation.Resource;
    import java.util.Map;
    ​
    /**
     * @author zouwei
     * @className WalletTccActionImpl
     * @date: 2022/10/10 21:05
     * @description:
     */
    @Component
    public class WalletTccActionImpl implements IWalletTccAction {
      // 用来调用数据库
      @Resource
      private WalletTccEnhanceMapper walletTccEnhanceMapper;
    ​
      @Override
      public boolean prepareDeductMoney(BusinessActionContext businessActionContext, String userId, Long amount) {
        String xid = businessActionContext.getXid();
        // 幂等性判断
        if (TccActionResultWrap.hasPrepareResult(xid)) {
          return true;
        }
        // 避免空悬挂,已经执行过回滚了就不能再预留资源
        if (TccActionResultWrap.hasRollbackResult(xid) || TccActionResultWrap.hasCommitResult(xid)) {
          return false;
        }
        // 预留资源
        // 相关sql: update wallet_tcc_tbl set money = money - #{amount,jdbcType=INTEGER}, pre_money = pre_money + #{amount,jdbcType=INTEGER} where user_id = #{userId,jdbcType=VARCHAR} and money <![CDATA[ >= ]]>#{amount,jdbcType=INTEGER}
        boolean result = walletTccEnhanceMapper.prepareDeductMoney(userId, amount) > 0;
        // 记录执行结果:xid:result
        // 以便回滚时判断是否是空回滚
        TccActionResultWrap.prepareSuccess(xid);
        return result;
      }
    ​
      // 保证提交逻辑的原子性
      @Transactional
      @Override
      public boolean commitDeductMoney(BusinessActionContext businessActionContext) {
        String xid = businessActionContext.getXid();
        // 幂等性判断
        if (TccActionResultWrap.hasCommitResult(xid)) {
          return true;
        }
        Map<String, Object> actionContext = businessActionContext.getActionContext();
        String userId = (String) actionContext.get("userId");
        long amount = (Integer) actionContext.get("amount");
        // 执行提交操作,扣除预留款
        // 相关sql: update wallet_tcc_tbl set pre_money = pre_money - #{amount,jdbcType=INTEGER} where user_id = #{userId,jdbcType=VARCHAR} and pre_money <![CDATA[ >= ]]>#{amount,jdbcType=INTEGER}
        boolean result = walletTccEnhanceMapper.commitDeductMoney(userId, amount) > 0;
        // 清除预留结果
        TccActionResultWrap.removePrepareResult(xid);
        // 设置提交结果
        TccActionResultWrap.commitSuccess(xid);
        return result;
      }
    ​
      @Transactional
      @Override
      public boolean rollbackDeductMoney(BusinessActionContext businessActionContext) {
        String xid = businessActionContext.getXid();
        // 幂等性判断
        if (TccActionResultWrap.hasRollbackResult(xid)) {
          return true;
        }
        // 没有预留资源结果,回滚不做任何处理;
        if (!TccActionResultWrap.hasPrepareResult(xid)) {
          // 设置回滚结果,防止空悬挂
          TccActionResultWrap.rollbackResult(xid);
          return true;
        }
        // 执行回滚
        Map<String, Object> actionContext = businessActionContext.getActionContext();
        String userId = (String) actionContext.get("userId");
        long amount = (Integer) actionContext.get("amount");
        // 相关sql: update wallet_tcc_tbl set money = money + #{amount,jdbcType=INTEGER}, pre_money = pre_money - #{amount,jdbcType=INTEGER} where user_id = #{userId,jdbcType=VARCHAR} and pre_money <![CDATA[ >= ]]>#{amount,jdbcType=INTEGER}
        boolean result = walletTccEnhanceMapper.rollbackDeductMoney(userId, amount) > 0;
        // 清除预留结果
        TccActionResultWrap.removePrepareResult(xid);
        // 设置回滚结果
        TccActionResultWrap.rollbackResult(xid);
        return result;
      }
    }
    

1.预扣款逻辑就是先把订单总金额从money字段下挪到pre_money下,前提是当前账户中有足够的金额可以扣除;

2.当账户有足够的金额时,那么意味着预扣款成功,这笔钱是其他事务不能操作的,只能静静地等待TM发起分布式事务提交或回滚指令;

3.一旦接收到提交指令,那么Seata就会调用commitDeductMoney方法把已经扣除的金额从pre_money扣掉,这就意味着这笔钱真正地被花掉了;

4.如果接收到回滚指令,说明下单失败,我们就需要把预扣款的钱还给客户,此时Seata就会调用rollbackDeductMoney方法,把之前挪到pre_money下的金额加回到总金额money中,用户账户里的钱就回来了。

Order服务

import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
​
/**
 * @author zouwei
 * @className IOrderTccAction
 * @date: 2022/10/11 12:58
 * @description:
 */
@LocalTCC
public interface IOrderTccAction {
  /**
   * 预创建订单
   *
   * @param businessActionContext
   * @param userId
   * @param commodityCode
   * @param count
   * @param unitPrice
   * @return
   */
  @TwoPhaseBusinessAction(name = "prepareOrder", commitMethod = "commitOrder", rollbackMethod = "rollbackOrder")
  boolean prepareOrder(BusinessActionContext businessActionContext,
                       @BusinessActionContextParameter(paramName = "userId") String userId,
                       @BusinessActionContextParameter(paramName = "userId") String commodityCode,
                       @BusinessActionContextParameter(paramName = "userId") int count,
                       @BusinessActionContextParameter(paramName = "userId") long unitPrice);
​
  /**
   * 订单生效
   *
   * @param businessActionContext
   * @return
   */
  boolean commitOrder(BusinessActionContext businessActionContext);
​
  /**
   * 回滚预创建订单
   *
   * @param businessActionContext
   * @return
   */
  boolean rollbackOrder(BusinessActionContext businessActionContext);
}
import com.example.awesomeorder.dao.entity.OrderTcc;
import com.example.awesomeorder.dao.mapper.OrderTccMapper;
import com.example.awesomeorder.tcc.IOrderTccAction;
import com.example.awesomeorder.tcc.TccActionResultWrap;
import io.seata.rm.tcc.api.BusinessActionContext;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
​
import javax.annotation.Resource;
import java.time.LocalDateTime;
​
/**
 * @author zouwei
 * @className OrderTccActionImpl
 * @date: 2022/10/11 13:05
 * @description:
 */
@Component
public class OrderTccActionImpl implements IOrderTccAction {
​
  @Resource
  private OrderTccMapper orderTccMapper;
​
​
  @Override
  public boolean prepareOrder(BusinessActionContext businessActionContext, String userId, String commodityCode, int count, long unitPrice) {
    String xid = businessActionContext.getXid();
    // 幂等性判断
    if (TccActionResultWrap.hasPrepareResult(xid)) {
      return true;
    }
    // 避免空悬挂,已经执行过回滚了就不能再预留资源
    if (TccActionResultWrap.hasRollbackResult(xid) || TccActionResultWrap.hasCommitResult(xid)) {
      return false;
    }
    // 准备预创建订单
    OrderTcc order = new OrderTcc();
    // 创建时间
    order.setCreateTime(LocalDateTime.now());
    // 用户ID
    order.setUserId(userId);
    // 数量
    order.setCount(count);
    // 商品编码
    order.setCommodityCode(commodityCode);
    // 单价
    order.setUnitPrice(unitPrice);
    // 设置状态
    order.setStatus("预创建");
    // 创建订单
    boolean result = orderTccMapper.insert(order) > 0;
    // 记录主键ID,为了传递给提交或回滚
    businessActionContext.addActionContext("id", order.getId());
    // 记录执行结果:xid:result
    // 以便回滚时判断是否是空回滚
    TccActionResultWrap.prepareSuccess(xid);
    return result;
  }
​
  @Transactional
  @Override
  public boolean commitOrder(BusinessActionContext businessActionContext) {
    String xid = businessActionContext.getXid();
    // 幂等性判断
    if (TccActionResultWrap.hasCommitResult(xid)) {
      return true;
    }
    // 修改预留资源状态
    Integer id = (Integer) businessActionContext.getActionContext("id");
    OrderTcc row = new OrderTcc();
    row.setId(id);
    row.setStatus("成功");
    boolean result = orderTccMapper.updateByPrimaryKeySelective(row) > 0;
    // 清除预留结果
    TccActionResultWrap.removePrepareResult(xid);
    // 设置提交结果
    TccActionResultWrap.commitSuccess(xid);
    return result;
  }
​
  @Transactional
  @Override
  public boolean rollbackOrder(BusinessActionContext businessActionContext) {
    String xid = businessActionContext.getXid();
    // 幂等性判断
    if (TccActionResultWrap.hasRollbackResult(xid)) {
      return true;
    }
    // 没有预留资源结果,回滚不做任何处理;
    if (!TccActionResultWrap.hasPrepareResult(xid)) {
      // 设置回滚结果,防止空悬挂
      TccActionResultWrap.rollbackResult(xid);
      return true;
    }
    // 执行回滚,删除预创建订单
    Integer id = (Integer) businessActionContext.getActionContext("id");
    boolean result = orderTccMapper.deleteByPrimaryKey(id) > 0;
​
    // 清除预留结果
    TccActionResultWrap.removePrepareResult(xid);
    // 设置回滚结果
    TccActionResultWrap.rollbackResult(xid);
    return result;
  }
}

1.在预创建订单服务中,我们使用了一个status字段来表示订单的中间状态,还未生效的订单状态为预创建;

2.当收到提交请求时,我们将订单状态置为成功

3.当收到回滚请求时,我们删除之前预创建的订单即可;

Storage服务

import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
​
/**
 * @author zouwei
 * @className IStorageTccAction
 * @date: 2022/10/11 13:57
 * @description:
 */
@LocalTCC
public interface IStorageTccAction {
  /**
   * 预扣库存
   *
   * @param businessActionContext
   * @param commodityCode
   * @param count
   * @return
   */
  @TwoPhaseBusinessAction(name = "prepareDeductStock", commitMethod = "commitDeductStock", rollbackMethod = "rollbackDeductStock")
  boolean prepareDeductStock(BusinessActionContext businessActionContext,
                             @BusinessActionContextParameter("commodityCode") String commodityCode,
                             @BusinessActionContextParameter("count") int count);
​
  /**
   * 提交被扣库存
   *
   * @param businessActionContext
   * @return
   */
  boolean commitDeductStock(BusinessActionContext businessActionContext);
​
  /**
   * 回滚被扣库存
   *
   * @param businessActionContext
   * @return
   */
  boolean rollbackDeductStock(BusinessActionContext businessActionContext);
}
import com.example.awesomestorage.dao.mapper.StockTccEnhanceMapper;
import com.example.awesomestorage.tcc.IStorageTccAction;
import com.example.awesomestorage.tcc.TccActionResultWrap;
import io.seata.rm.tcc.api.BusinessActionContext;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
​
import javax.annotation.Resource;
import java.util.Map;
​
/**
 * @author zouwei
 * @className StorageTccActionImpl
 * @date: 2022/10/11 14:00
 * @description:
 */
@Component
public class StorageTccActionImpl implements IStorageTccAction {
​
  @Resource
  private StockTccEnhanceMapper stockTccEnhanceMapper;
​
  @Override
  public boolean prepareDeductStock(BusinessActionContext businessActionContext, String commodityCode, int count) {
    String xid = businessActionContext.getXid();
    // 幂等性判断
    if (TccActionResultWrap.hasPrepareResult(xid)) {
      return true;
    }
    // 避免空悬挂,已经执行过回滚了就不能再预留资源
    if (TccActionResultWrap.hasRollbackResult(xid) || TccActionResultWrap.hasCommitResult(xid)) {
      return false;
    }
    // 预扣库存
    // 相关sql: update stock_tcc_tbl set count = count - #{count,jdbcType=INTEGER}, pre_deduct_count = pre_deduct_count + #{count,jdbcType=INTEGER} where commodity_code = #{commodityCode,jdbcType=VARCHAR} and count <![CDATA[ >= ]]> #{count,jdbcType=INTEGER}
    boolean result = stockTccEnhanceMapper.prepareDeductStock(commodityCode, count) > 0;
    // 记录执行结果:xid:result
    // 以便回滚时判断是否是空回滚
    TccActionResultWrap.prepareSuccess(xid);
    return result;
  }
​
  @Transactional
  @Override
  public boolean commitDeductStock(BusinessActionContext businessActionContext) {
    String xid = businessActionContext.getXid();
    // 幂等性判断
    if (TccActionResultWrap.hasCommitResult(xid)) {
      return true;
    }
    Map<String, Object> actionContext = businessActionContext.getActionContext();
    String commodityCode = (String) actionContext.get("commodityCode");
    int count = (int) actionContext.get("count");
    // 执行提交操作,扣除库存
    // 相关sql:update stock_tcc_tbl set pre_deduct_count = pre_deduct_count - #{count,jdbcType=INTEGER} where commodity_code = #{commodityCode,jdbcType=VARCHAR} and pre_deduct_count <![CDATA[ >= ]]> #{count,jdbcType=INTEGER}
    boolean result = stockTccEnhanceMapper.commitDeductStock(commodityCode, count) > 0;
    // 清除预留结果
    TccActionResultWrap.removePrepareResult(xid);
    // 设置提交结果
    TccActionResultWrap.commitSuccess(xid);
    return result;
  }
​
  @Transactional
  @Override
  public boolean rollbackDeductStock(BusinessActionContext businessActionContext) {
    String xid = businessActionContext.getXid();
    // 幂等性判断
    if (TccActionResultWrap.hasRollbackResult(xid)) {
      return true;
    }
    // 没有预留资源结果,回滚不做任何处理;
    if (!TccActionResultWrap.hasPrepareResult(xid)) {
      // 设置回滚结果,防止空悬挂
      TccActionResultWrap.rollbackResult(xid);
      return true;
    }
    // 执行回滚
    Map<String, Object> actionContext = businessActionContext.getActionContext();
    String commodityCode = (String) actionContext.get("commodityCode");
    int count = (int) actionContext.get("count");
    // 相关sql: update stock_tcc_tbl set count = count + #{count,jdbcType=INTEGER}, pre_deduct_count = pre_deduct_count - #{count,jdbcType=INTEGER} where commodity_code = #{commodityCode,jdbcType=VARCHAR} and pre_deduct_count <![CDATA[ >= ]]> #{count,jdbcType=INTEGER}
    boolean result = stockTccEnhanceMapper.rollbackDeductStock(commodityCode, count) > 0;
    // 清除预留结果
    TccActionResultWrap.removePrepareResult(xid);
    // 设置回滚结果
    TccActionResultWrap.rollbackResult(xid);
    return result;
  }
}

1.预扣减库存的原理和预扣款原理一致,我们需要额外给出一个字段pre_deduct_count来放预扣的库存数量;通过减少总库存数count,把被扣库存预留到pre_deduct_count中达到预扣的效果;

2.当需要提交时,我们才真正地从pre_deduct_count中扣除我们需要的库存数;

3.当接收到回滚请求时,我们把已经预扣的数量还给总库存count

再补充一个TccActionResultWrap,这个类用来记录TCC Action的状态,用来临时解决幂等性空回滚空悬挂问题。

import org.apache.commons.lang3.StringUtils;
​
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
​
/**
 * @author zouwei
 * @className TccActionResultWrap
 * @date: 2022/10/11 13:00
 * @description:
 */
public class TccActionResultWrap {
​
  private static final Map<String, String> prepareResult = new ConcurrentHashMap<>();
​
  private static final Map<String, String> commitResult = new ConcurrentHashMap<>();
​
  private static final Map<String, String> rollbackResult = new ConcurrentHashMap<>();
​
  public static void prepareSuccess(String xid) {
    prepareResult.put(xid, "success");
  }
​
  public static void commitSuccess(String xid) {
    commitResult.put(xid, "success");
  }
​
  public static void rollbackSuccess(String xid) {
    rollbackResult.put(xid, "success");
  }
​
  public static void removePrepareResult(String xid) {
    prepareResult.remove(xid);
  }
​
  public static void removeCommitResult(String xid) {
    commitResult.remove(xid);
  }
​
  public static void removeRollbackResult(String xid) {
    rollbackResult.remove(xid);
  }
​
  public static String prepareResult(String xid) {
    return prepareResult.get(xid);
  }
​
  public static String commitResult(String xid) {
    return commitResult.get(xid);
  }
​
  public static String rollbackResult(String xid) {
    return rollbackResult.get(xid);
  }
​
  public static boolean hasPrepareResult(String xid) {
    return StringUtils.isNotBlank(prepareResult(xid));
  }
​
  public static boolean hasCommitResult(String xid) {
    return StringUtils.isNotBlank(commitResult.get(xid));
  }
​
  public static boolean hasRollbackResult(String xid) {
    return StringUtils.isNotBlank(rollbackResult.get(xid));
  }
}
​

以上我们就把所有服务的TCC Action全部处理完毕,我们下面来看看如何使用这些TCC Action:

Service如何使用TCC Action

Account服务

import com.example.awesomeaccount.service.IWalletService;
import com.example.awesomeaccount.tcc.IWalletTccAction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
​
import javax.annotation.Resource;
​
/**
 * @author zouwei
 * @className WalletServiceImpl
 * @date: 2022/9/27 21:20
 * @description:
 */
@Service
public class WalletServiceImpl implements IWalletService {
  
  @Autowired
  private IWalletTccAction walletTccAction;
​
  @Transactional
  @Override
  public Boolean deductMoney(String userId, long amount) {
    return walletTccAction.prepareDeductMoney(null, userId, amount);
  }
}

我们只要在service中直接调用IWalletTccAction接口中的prepareDeductMoney方法执行预扣款即可,这个prepareDeductMoney方法其实就代表一个真正扣款的功能,因为在所有的调用链都调用成功后,Seata会自动调用TCC Action中定义的提交逻辑,以达到一个最终的扣款目的。

注意:参数中的BusinessActionContext不需要开发人员自己传递,直接给null即可,Seata会自动处理。

Order服务

import com.example.awesomeorder.api.StockApiClient;
import com.example.awesomeorder.service.IOrderService;
import com.example.awesomeorder.tcc.IOrderTccAction;
import com.example.storageapi.model.OrderInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
​
import javax.annotation.Resource;
​
/**
 * @author zouwei
 * @className OrderServiceImpl
 * @date: 2022/9/27 22:47
 * @description:
 */
@Service
public class OrderServiceImpl implements IOrderService {
  
  @Autowired
  private IOrderTccAction orderTccAction;
​
  @Resource
  private StockApiClient stockApiClient;
​
  /**
   * 创建订单
   *
   * @param userId
   * @param commodityCode
   * @param count
   * @param unitPrice
   * @return
   */
  @Transactional
  @Override
  public Boolean createOrder(String userId, String commodityCode, int count, long unitPrice) {
    // 构建待扣减的库存信息
    OrderInfo orderInfo = new OrderInfo();
    // 设置商品编码
    orderInfo.setCommodityCode(commodityCode);
    // 设置需要扣减的数量
    orderInfo.setCount(count);
    // 先预扣减库存
    if (stockApiClient.deductStock(orderInfo)) {
      // 预创建订单
      return orderTccAction.prepareOrder(null, userId, commodityCode, count, unitPrice);
    }
    // 扣减库存失败,订单创建失败
    return Boolean.FALSE;
  }
}

Order服务的Service实现和Account服务差不多,TCC Action也可以和Spring Bean一样使用;

Storage服务:

import com.example.awesomestorage.service.IStockService;
import com.example.awesomestorage.tcc.IStorageTccAction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
​
/**
 * @author zouwei
 * @className StockServiceImpl
 * @date: 2022/9/28 00:06
 * @description:
 */
@Service
public class StockServiceImpl implements IStockService {
  
  @Autowired
  private IStorageTccAction storageTccAction;
​
  @Transactional
  @Override
  public Boolean deductStock(String commodityCode, int count) {
    return storageTccAction.prepareDeductStock(null, commodityCode, count);
  }
}

Business下单逻辑

import com.example.accountapi.model.AmountInfo;
import com.example.awesomebusiness.api.OrderApiClient;
import com.example.awesomebusiness.api.WalletApiClient;
import com.example.awesomebusiness.service.ShoppingCartService;
import com.example.orderapi.model.OrderInfo;
import io.seata.core.exception.GlobalTransactionException;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.stereotype.Service;
​
import javax.annotation.Resource;
​
/**
 * @author zouwei
 * @className ShoppingCartServiceImpl
 * @date: 2022/9/18 14:01
 * @description:
 */
@Service
public class ShoppingCartServiceImpl implements ShoppingCartService {
  // 钱包服务
  @Resource
  private WalletApiClient walletApiClient;
  // 订单服务
  @Resource
  private OrderApiClient orderApiClient;
​
  // 别忘记了这个注解,这是开启分布式事务的标记,前提是当前业务逻辑不处于任何的分布式事务当中才能开启新的分布式事务
  @GlobalTransactional
  public String placeOrder() throws GlobalTransactionException {
    // 模拟用户ID 123456,对应数据库初始化的用户ID
    String userId = "123456";
    // 构建订单数据
    OrderInfo orderInfo = new OrderInfo();
    // 数量15个
    orderInfo.setCount(15);
    // 商品编码,对应库存数据表的初始化数据
    orderInfo.setCommodityCode("CC-54321");
    // 单价299,默认是long类型,单位分;避免double精度丢失
    orderInfo.setUnitPrice(299);
    // 订单归属
    orderInfo.setUserId(userId);
    // 计算扣款金额,数量*单价
    long amount = orderInfo.getCount() * orderInfo.getUnitPrice();
    // 构建扣款数据
    AmountInfo amountInfo = new AmountInfo();
    // 设置扣款金额
    amountInfo.setAmount(amount);
    // 设置扣款主体
    amountInfo.setUserId(userId);
    // 先扣款,扣款成功就创建订单,扣减库存在创建订单的逻辑里面
    if (walletApiClient.deductMoney(amountInfo) && orderApiClient.createOrder(orderInfo)) {
      return "下单成功!";
    }
    // 1.扣款失败,抛异常,分布式事务回滚
    // 2.创建订单失败,抛异常,分布式事务回滚
    throw new GlobalTransactionException("下单失败!");
  }
}
  • 在Business下单逻辑中,和之前AT模式完全没有变化,所有的RPC依然还是使用的FeignClient;
  • @GlobalTransactional还是放在方法上面,代表这是TM角色,负责分布式事务的发起、提交和回滚;
  • XID的传递依然是RPC来传递;

至此,我们已经把所有的思路及相关核心代码编写完毕,有兴趣的小伙伴可以在github上下载该项目:awesome-seata