Seata的TCC模式与XA模式实战使用

SeataXA模式

XA协议最主要的作用是就是定义了RM-TM的交互接口,除此之外,还对两阶段提交协议进行了优化。



整体机制

在 Seata 定义的分布式事务框架内,利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种事务模式。

在这里插入图片描述



  • 执行阶段:

    • 可回滚:业务 SQL 操作放在 XA 分支中进行,由资源对 XA 协议的支持来保证 可回滚

    • 持久化:XA 分支完成后,执行 XA prepare,同样,由资源对 XA 协议的支持来保证 持久化

  • 完成阶段:

    • 分支提交:执行 XA 分支的 commit

    • 分支回滚:执行 XA 分支的 rollback

在执行阶段,会一直保持数据库连接对象,直到二阶段之后才会释放本地连接对象



AT和XA模式数据源代理机制对比

在这里插入图片描述



XA模式的使用

从编程模型上,XA 模式与 AT 模式保持完全一致。只需要修改数据源代理,即可实现 XA 模式与 AT 模式之间的切换

@Bean("dataSource")
public DataSource dataSource(DruidDataSource druidDataSource) {
    // DataSourceProxy for AT mode
    // return new DataSourceProxy(druidDataSource);

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



微服务整合SeataXA

Spring Cloud Alibaba整合Seata XA实战

对比Seata AT模式配置,只需修改两个地方:

  • 微服务数据库不需要undo_log表,undo_log表仅用于AT模式
  • 修改数据源代码模式为XA模式
seata:
  # 数据源代理模式 默认AT
  data-source-proxy-mode: XA

完整配置如下

seata:
  # 是否开启spring-boot自动装配,默认true,包括数据源的自动代理以及GlobalTransactionScanner初始化
  enabled: true
  # 数据源代理模式 默认AT
  data-source-proxy-mode: XA
  application-id: ${spring.application.name}
  # seata 服务分组,要与服务端配置service.vgroup_mapping的后缀对应
  tx-service-group: default_tx_group
  registry:
    # 指定nacos作为注册中心
    type: nacos
    nacos:
      application: seata-server
      server-addr:  nacos.mall.com:8848
      namespace:
      group: SEATA_GROUP
      username: nacos
      password: nacos

  config:
    # 指定nacos作为配置中心
    type: nacos
    nacos:
      server-addr:  nacos.mall.com:8848
      namespace: 7e838c12-8554-4231-82d5-6d93573ddf32
      group: SEATA_GROUP
      data-id: seataServer.properties
      username: nacos
      password: nacos



如果使用SeataXA模式,在全局事务开启方,它自己本方法也要操作数据的的话,那么该方法处了要使用@GlobalTransactional注解之外还要使用@Transactional

@Override
@GlobalTransactional(name="createOrder",rollbackFor=Exception.class)
@Transactional
public Order saveOrder(OrderVo orderVo) {
    log.info("=============用户下单=================");
    log.info("当前 XID: {}", RootContext.getXID());

    // 保存订单
    Order order = new Order();
    order.setUserId(orderVo.getUserId());
    order.setCommodityCode(orderVo.getCommodityCode());
    order.setCount(orderVo.getCount());
    order.setMoney(orderVo.getMoney());
    order.setStatus(OrderStatus.INIT.getValue());

    // 本方法操作数据库
    Integer saveOrderRecord = orderMapper.insert(order);
    log.info("保存订单{}", saveOrderRecord > 0 ? "成功" : "失败");

    // OpenFeign 扣减库存
    storageFeignService.deduct(orderVo.getCommodityCode(), orderVo.getCount());

    // OpenFeign 扣减余额
    boolean debit= accountFeignService.debit(orderVo.getUserId(), orderVo.getMoney());

    //        if(!debit){
    //            // 解决 feign整合sentinel降级导致Seata失效的处理
    //            throw new RuntimeException("账户服务异常降级了");
    //        }

    //更新订单
    Integer updateOrderRecord = orderMapper.updateOrderStatus(order.getId(),OrderStatus.SUCCESS.getValue());
    log.info("更新订单id:{} {}", order.getId(), updateOrderRecord > 0 ? "成功" : "失败");

    return order;

}



SeataTCC模式

什么是TCC

TCC 基于分布式事务中的二阶段提交协议实现,它的全称为 Try-Confirm-Cancel,即资源预留(Try)、确认操作(Confirm)、取消操作(Cancel),他们的具体含义如下:

  1. Try:对业务资源的检查并预留;
  2. Confirm:对业务处理进行提交,即 commit 操作,只要 Try 成功,那么该步骤一定成功;
  3. Cancel:对业务处理进行取消,即回滚操作,该步骤回对 Try 预留的资源进行释放。

在这里插入图片描述



  • XA是资源层面的分布式事务,强一致性,在两阶段提交的整个过程中,一直会持有资源的锁。
  • TCC是业务层面的分布式事务,最终一致性,不会一直持有资源的锁。



TCC 是一种侵入式的分布式事务解决方案,以上三个操作都需要业务系统自行实现,对业务系统有着非常大的入侵性,设计相对复杂,但优点是 TCC 完全不依赖数据库,能够实现跨数据库、跨应用资源管理,对这些不同数据访问通过侵入式的编码方式实现一个原子操作,更好地解决了在各种复杂业务场景下的分布式事务问题。



常见开源TCC框架:

  • Seata TCC

  • Hmily

  • Tcc-Transaction

  • ByteTCC

  • EasyTransaction



以用户下单为例

在TCC模式下我们一般对数值型字段采用冻结,对非数值型添加status状态字段。比如我在库存量字段旁加一个冻结字段,在try阶段我扣减库存字段的值,在冻结字段位置进行增加,confirm阶段就直接处理冻结字段的数值,cancel阶段就减冻结字段的值 加库存字段的值

在金融领域,转账业务下,我们一般是在confirm阶段才会去为对方账号进行余额相加的操作。避免出现try阶段把钱转过去了,对方去使用钱,结果最后转账放要进行cancel,对方账号的钱缺已经使用了。

try-commit

try 阶段首先进行预留资源,然后在 commit 阶段扣除资源。如下图:

在这里插入图片描述



try-cancel

try 阶段首先进行预留资源,预留资源时扣减库存失败导致全局事务回滚,在 cancel 阶段释放资源。如下图:

在这里插入图片描述



Seata TCC 模式

一个分布式的全局事务,整体是 两阶段提交 的模型。

全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:

  • 一阶段 prepare 行为
  • 二阶段 commit 或 rollback 行为

在这里插入图片描述



在Seata中,AT模式与TCC模式事实上都是两阶段提交的具体实现,他们的区别在于:

AT 模式基于 支持本地 ACID 事务的关系型数据库:

  • 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
  • 二阶段 commit 行为:马上成功结束,自动异步批量清理回滚日志。
  • 二阶段 rollback 行为:通过回滚日志,自动生成补偿操作,完成数据回滚。

相应的,TCC 模式不依赖于底层数据资源的事务支持:

  • 一阶段 prepare 行为:调用自定义的 prepare 逻辑。
  • 二阶段 commit 行为:调用自定义的 commit 逻辑。
  • 二阶段 rollback 行为:调用自定义的 rollback 逻辑。

简单点概括,Seata的TCC模式就是手工的AT模式,它允许你自定义两阶段的处理逻辑而不依赖AT模式的undo_log表。



Seata TCC模式接口改造

假设现有一个业务需要同时使用服务 A 和服务 B 完成一个事务操作,我们在服务 A 定义该服务的一个 TCC 接口:

public interface TccActionOne {
    @TwoPhaseBusinessAction(name = "prepare", commitMethod = "commit", rollbackMethod = "rollback")
    public boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "a") String a);

    public boolean commit(BusinessActionContext actionContext);

    public boolean rollback(BusinessActionContext actionContext);
}



同样,在服务 B 定义该服务的一个 TCC 接口:

public interface TccActionTwo {
    @TwoPhaseBusinessAction(name = "prepare", commitMethod = "commit", rollbackMethod = "rollback")
    public void prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "b") String b);

    public void commit(BusinessActionContext actionContext);

    public void rollback(BusinessActionContext actionContext);
}



在业务所在系统中开启全局事务并执行服务 A 和服务 B 的 TCC 预留资源方法:

@GlobalTransactional
public String doTransactionCommit(){
    //服务A事务参与者
    tccActionOne.prepare(null,"one");
    //服务B事务参与者
    tccActionTwo.prepare(null,"two");
}

以上就是使用 Seata TCC 模式实现一个全局事务的例子,TCC 模式同样使用 @GlobalTransactional 注解开启全局事务,而服务 A 和服务 B 的 TCC 接口为事务参与者,Seata 会把一个 TCC 接口当成一个 Resource,也叫 TCC Resource。



TCC如何控制异常

在 TCC 模型执行的过程中,还可能会出现各种异常,其中最为常见的有空回滚、幂等、悬挂等。TCC 模式是分布式事务中非常重要的事务模式,但是幂等、悬挂和空回滚一直是 TCC 模式需要考虑的问题,Seata 框架在 1.5.1 版本完美解决了这些问题。



空回滚

空回滚指的是在一个分布式事务中,在没有调用参与方的 Try 方法的情况下,TM 驱动二阶段回滚调用了参与方的 Cancel 方法。

空回滚产生原因分析

在这里插入图片描述



全局事务开启,参与者A向TC注册了分支事务会立刻执行参与者一阶段RPC方法,如果此时参与者A机器宕机或网络异常,导致RPC方法调用失败 未执行。但此时全局事务已经开启,TC最终会全局事务回滚,调用参与者A进行回滚Cannel方法,从而造成了空回滚



要想防止空回滚,那么必须在 Cancel 方法中识别这是一个空回滚,Seata 是如何做的呢?

Seata 的做法是新增一个 TCC 事务控制表,包含事务的 XID 和 BranchID 信息,在 Try 方法执行时插入一条记录,表示一阶段执行了,执行 Cancel 方法时读取这条记录,如果记录不存在,说明 Try 方法没有执行。



幂等

幂等问题指的是 TC 重复进行二阶段提交,因此 Confirm/Cancel 接口需要支持幂等处理,即不会产生资源重复提交或者重复释放。

那么幂等问题是如何产生的呢

在这里插入图片描述



参与者 A 执行完二阶段之后,由于网络抖动或者宕机问题,会造成 TC 收不到参与者 A 执行二阶段的返回结果,TC 会重复发起调用,直到二阶段执行结果成功。



Seata 是如何处理幂等问题的呢?

同样的也是在 TCC 事务控制表中增加一个记录状态的字段 status,该字段有 3 个值,分别为:

  1. tried:1
  2. committed:2
  3. rollbacked:3

二阶段 Confirm/Cancel 方法执行后,将状态改为 committed 或 rollbacked 状态。当重复调用二阶段 Confirm/Cancel 方法时,判断事务状态即可解决幂等问题。



悬挂

悬挂指的是二阶段 Cancel 方法比 一阶段 Try 方法优先执行,由于允许空回滚的原因,在执行完二阶段 Cancel 方法之后直接空回滚返回成功,此时全局事务已结束,但是由于 Try 方法随后执行,这就会造成一阶段 Try 方法预留的资源永远无法提交和释放了。

那么悬挂是如何产生的呢?

在这里插入图片描述



如上图所示,在执行参与者 A 的一阶段 Try 方法时,出现网路拥堵,由于 Seata 全局事务有超时限制,执行 Try 方法超时后,TM 决议全局回滚,回滚完成后如果此时 RPC 请求才到达参与者 A,执行 Try 方法进行资源预留,从而造成悬挂。



Seata 是怎么处理悬挂的呢?

在 TCC 事务控制表记录状态的字段 status 中增加一个状态:

  • suspended:4

当执行二阶段 Cancel 方法时,如果发现 TCC 事务控制表没有相关记录,说明二阶段 Cancel 方法优先一阶段 Try 方法执行,因此插入一条 status=4 状态的记录,当一阶段 Try 方法后面执行时,判断 status=4 ,则说明有二阶段 Cancel 已执行,并返回 false 以阻止一阶段 Try 方法执行成功。



微服务整合SeataTCC

用户下单,整个业务逻辑由三个微服务构成:

  • 库存服务:对给定的商品扣除库存数量。
  • 订单服务:根据采购需求创建订单。
  • 帐户服务:从用户帐户中扣除余额。

在这里插入图片描述



环境准备

父pom指定微服务版本

Spring Cloud Alibaba VersionSpring Cloud VersionSpring Boot VersionSeata Version
2022.0.0.02022.0.03.0.21.7.0
  • 启动Seata Server(TC)端,Seata Server使用nacos作为配置中心和注册中心

  • 启动nacos服务



微服务导入seata依赖

spring-cloud-starter-alibaba-seata内部集成了seata,并实现了xid传递

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



微服务application.yml中添加seata配置

seata:
  application-id: ${spring.application.name}
  # seata 服务分组,要与服务端配置service.vgroup_mapping的后缀对应
  tx-service-group: default_tx_group
  registry:
    # 指定nacos作为注册中心
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      namespace:
      group: SEATA_GROUP
      username: nacos
      password: nacos

  config:
    # 指定nacos作为配置中心
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace: 7e838c12-8554-4231-82d5-6d93573ddf32
      group: SEATA_GROUP
      data-id: seataServer.properties
      username: nacos
      password: nacos

注意:请确保client与server的注册中心和配置中心namespace和group一致



定义TCC接口

TCC相关注解如下:

  • @LocalTCC 适用于SpringCloud+Feign模式下的TCC,@LocalTCC一定需要注解在接口上,此接口可以是寻常的业务接口,只要实现了TCC的两阶段提交对应方法便可
  • @TwoPhaseBusinessAction 注解try方法,其中name为当前tcc方法的bean名称,写方法名便可(全局唯一),commitMethod指向提交方法,rollbackMethod指向事务回滚方法。指定好三个方法之后,seata会根据全局事务的成功或失败,去帮我们自动调用提交方法或者回滚方法。
  • @BusinessActionContextParameter 注解可以将参数传递到二阶段(commitMethod/rollbackMethod)的方法。
  • BusinessActionContext 便是指TCC事务上下文

订单Order接口,首先自己先生成一条订单信息,调用库存storage服务扣减库存、调用用户account服务进行扣款账户余额。



首先的是订单的接口:

  • 在try阶段会插入一条订单记录,为原始的订单表添加一个status代表状态的字段
  • 在commit阶段,将status的状态修改为success状态
  • 在rollback阶段,将status的状态改为fail状态
// 全局事务 发起者
public interface BussinessService {

    /**
     * 保存订单
     */
    Order saveOrder(OrderVo orderVo) ;
}
@Service
@Slf4j
public class BusinessServiceImpl implements BussinessService {

    @Autowired
    private AccountFeignService accountFeignService;

    @Autowired
    private StorageFeignService storageFeignService;

    // OrderService 就是我们接下来要定义的
    @Autowired
    private OrderService orderService;


    // 全局事务发起者,使用@GlobalTransaction注解
    @Override
    @GlobalTransactional(name="createOrder",rollbackFor=Exception.class)
    public Order saveOrder(OrderVo orderVo) {
        log.info("=============用户下单=================");
        log.info("当前 XID: {}", RootContext.getXID());

        //获取全局唯一订单号  测试使用
        Long orderId = UUIDGenerator.generateUUID();

        //阶段一: 创建订单
        Order order = orderService.prepareSaveOrder(orderVo,orderId);

        //扣减库存
        storageFeignService.deduct(orderVo.getCommodityCode(), orderVo.getCount());
        //扣减余额
        accountFeignService.debit(orderVo.getUserId(), orderVo.getMoney());

        return order;
    }
}
/**
 * 通过 @LocalTCC 这个注解,RM 初始化的时候会向 TC 注册一个分支事务。
 */
@LocalTCC
public interface OrderService {

    /**
     * TCC的try方法:保存订单信息,状态为支付中
     *
     * 定义两阶段提交,在try阶段通过@TwoPhaseBusinessAction注解定义了分支事务的 resourceId,commit和 cancel 方法
     *  name = 该tcc的bean名称,全局唯一
     *  commitMethod = commit 为二阶段确认方法
     *  rollbackMethod = rollback 为二阶段取消方法
     *  BusinessActionContextParameter注解 传递参数到二阶段中,二阶段方法中通过paramName属性指定的key获取到值
     *  useTCCFence seata1.5.1的新特性,用于解决TCC幂等,悬挂,空回滚问题,需在服务端数据库中增加日志表tcc_fence_log
     */
    @TwoPhaseBusinessAction(name = "prepareSaveOrder", commitMethod = "commit", rollbackMethod = "rollback", useTCCFence = true)
    Order prepareSaveOrder(OrderVo orderVo, @BusinessActionContextParameter(paramName = "orderId") Long orderId);

    /**
     *
     * TCC的confirm方法:订单状态改为支付成功
     *
     * 二阶段确认方法可以另命名,但要保证与commitMethod一致
     * context可以传递try方法的参数
     *
     * @param actionContext
     * @return
     */
    boolean commit(BusinessActionContext actionContext);

    /**
     * TCC的cancel方法:订单状态改为支付失败
     * 二阶段取消方法可以另命名,但要保证与rollbackMethod一致
     *
     * @param actionContext
     * @return
     */
    boolean rollback(BusinessActionContext actionContext);
}
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
    
    @Autowired
    private OrderMapper orderMapper;

    @Override
    @Transactional(rollbackFor=Exception.class)
    public Order prepareSaveOrder(OrderVo orderVo,@BusinessActionContextParameter(paramName = "orderId") Long orderId) {

        // 保存订单
        Order order = new Order();
        order.setId(orderId);
        order.setUserId(orderVo.getUserId());
        order.setCommodityCode(orderVo.getCommodityCode());
        order.setCount(orderVo.getCount());
        order.setMoney(orderVo.getMoney());
        order.setStatus(OrderStatus.INIT.getValue());
        Integer saveOrderRecord = orderMapper.insert(order);
        log.info("保存订单{}", saveOrderRecord > 0 ? "成功" : "失败");

        return order;
    }

    @Override
    public boolean commit(BusinessActionContext actionContext) {


        // 获取订单id
        // 因为接口的方法中使用了@BusinessActionContextParameter(paramName = "orderId"),所以这里就能从actionContext中获取
        long orderId = Long.parseLong(actionContext.getActionContext("orderId").toString());
        //更新订单状态为支付成功
        Integer updateOrderRecord = orderMapper.updateOrderStatus(orderId,OrderStatus.SUCCESS.getValue());
        log.info("更新订单id:{} {}", orderId, updateOrderRecord > 0 ? "成功" : "失败");

        return true;
    }

    // TCC模式  进行手动补偿机制
    @Override
    public boolean rollback(BusinessActionContext actionContext) {

        //获取订单id
        long orderId = Long.parseLong(actionContext.getActionContext("orderId").toString());
        //更新订单状态为支付失败
        Integer updateOrderRecord = orderMapper.updateOrderStatus(orderId,OrderStatus.FAIL.getValue());
        log.info("更新订单id:{} {}", orderId, updateOrderRecord > 0 ? "成功" : "失败");

        return true;
    }


}



库存storage服务:

  • 在原库存表中新增一个冻结库存的字段,先检查库存是否充足
  • try方法中,库存字段中的值 -count 而冻结库存的值+count
  • 在commit方法中,直接操作冻结库存字段的值,冻结存储的值-count
  • 在rollback方法中,需要库存字段中的值 +count 而冻结库存的值-count
/**
 * 通过 @LocalTCC 这个注解,RM 初始化的时候会向 TC 注册一个分支事务。
 */
@LocalTCC
public interface StorageService {

    /**
     * Try: 库存-扣减数量,冻结库存+扣减数量
     *
     * 定义两阶段提交,在try阶段通过@TwoPhaseBusinessAction注解定义了分支事务的 resourceId,commit和 cancel 方法
     *  name = 该tcc的bean名称,全局唯一
     *  commitMethod = commit 为二阶段确认方法
     *  rollbackMethod = rollback 为二阶段取消方法
     *  BusinessActionContextParameter注解 传递参数到二阶段中
     *
     * @param commodityCode 商品编号
     * @param count 扣减数量
     * @return
     */
    @TwoPhaseBusinessAction(name = "deduct", commitMethod = "commit", rollbackMethod = "rollback", useTCCFence = true)
    boolean deduct(@BusinessActionContextParameter(paramName = "commodityCode") String commodityCode,
                   @BusinessActionContextParameter(paramName = "count") int count);

    /**
     *
     * Confirm: 冻结库存-扣减数量
     * 二阶段确认方法可以另命名,但要保证与commitMethod一致
     * context可以传递try方法的参数
     *
     * @param actionContext
     * @return
     */
    boolean commit(BusinessActionContext actionContext);

    /**
     * Cancel: 库存+扣减数量,冻结库存-扣减数量
     * 二阶段取消方法可以另命名,但要保证与rollbackMethod一致
     *
     * @param actionContext
     * @return
     */
    boolean rollback(BusinessActionContext actionContext);
}
@Service
@Slf4j
public class StorageServiceImpl implements StorageService {
    
    @Autowired
    private StorageMapper storageMapper;
    
    @Transactional
    @Override
    public boolean deduct(String commodityCode, int count){
        log.info("=============冻结库存=================");
        log.info("当前 XID: {}", RootContext.getXID());

        // 检查库存
        checkStock(commodityCode,count);
        
        log.info("开始冻结 {} 库存", commodityCode);
        //冻结库存
        Integer record = storageMapper.freezeStorage(commodityCode,count);
        log.info("冻结 {} 库存结果:{}", commodityCode, record > 0 ? "操作成功" : "扣减库存失败");
        return true;
    }

    @Override
    public boolean commit(BusinessActionContext actionContext) {
        log.info("=============扣减冻结库存=================");

        String commodityCode = actionContext.getActionContext("commodityCode").toString();
        int count = (int) actionContext.getActionContext("count");
        //扣减冻结库存
        storageMapper.reduceFreezeStorage(commodityCode,count);

        return true;
    }

    @Override
    public boolean rollback(BusinessActionContext actionContext) {
        log.info("=============解冻库存=================");

        String commodityCode = actionContext.getActionContext("commodityCode").toString();
        int count = (int) actionContext.getActionContext("count");
        //扣减冻结库存
        storageMapper.unfreezeStorage(commodityCode,count);

        return true;
    }

    private void checkStock(String commodityCode, int count){
        
        log.info("检查 {} 库存", commodityCode);
        Storage storage = storageMapper.findByCommodityCode(commodityCode);
        if (storage.getCount() < count) {
            log.warn("{} 库存不足,当前库存:{}", commodityCode, count);
            throw new RuntimeException("库存不足");
        }
        
    }
    
}



用户Account服务:

  • 在原有的余额字段之外,新增一个冻结余额字段。检查账户余额是否充足
  • try阶段,余额字段的值 -money ,冻结余额字段的值 +money
  • commit阶段,仅操作冻结余额字段,冻结余额字段的值 -money
  • rollback阶段,余额字段的值+money ,冻结余额字段的值 -money
/**
 * 通过 @LocalTCC 这个注解,RM 初始化的时候会向 TC 注册一个分支事务。
 */
@LocalTCC
public interface AccountService {

    /**
     * 用户账户扣款
     *
     * 定义两阶段提交,在try阶段通过@TwoPhaseBusinessAction注解定义了分支事务的 resourceId,commit和 cancel 方法
     *  name = 该tcc的bean名称,全局唯一
     *  commitMethod = commit 为二阶段确认方法
     *  rollbackMethod = rollback 为二阶段取消方法
     *
     * @param userId
     * @param money 从用户账户中扣除的金额
     * @return
     */
    @TwoPhaseBusinessAction(name = "debit", commitMethod = "commit", rollbackMethod = "rollback", useTCCFence = true)
    boolean debit(@BusinessActionContextParameter(paramName = "userId") String userId,
                  @BusinessActionContextParameter(paramName = "money") int money);

    /**
     * 提交事务,二阶段确认方法可以另命名,但要保证与commitMethod一致
     * context可以传递try方法的参数
     *
     * @param actionContext
     * @return
     */
    boolean commit(BusinessActionContext actionContext);

    /**
     * 回滚事务,二阶段取消方法可以另命名,但要保证与rollbackMethod一致
     *
     * @param actionContext
     * @return
     */
    boolean rollback(BusinessActionContext actionContext);
}
@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
    

    @Autowired
    private AccountMapper accountMapper;
    
    /**
     * 扣减用户金额
     * @param userId
     * @param money
     */
    @Transactional
    @Override
    public boolean debit(String userId, int money){
        log.info("=============冻结用户账户余额=================");
        log.info("当前 XID: {}", RootContext.getXID());
    
        // 检查余额
        checkBalance(userId, money);
        
        log.info("开始冻结用户 {} 余额", userId);
        //冻结金额
        Integer record = accountMapper.freezeBalance(userId,money);

        log.info("冻结用户 {} 余额结果:{}", userId, record > 0 ? "操作成功" : "扣减余额失败");
        return true;
    }

    @Override
    public boolean commit(BusinessActionContext actionContext) {
        log.info("=============扣减冻结金额=================");

        String userId = actionContext.getActionContext("userId").toString();
        int money = (int) actionContext.getActionContext("money");
        //扣减冻结金额
        accountMapper.reduceFreezeBalance(userId,money);

        return true;
    }

    @Override
    public boolean rollback(BusinessActionContext actionContext) {
        log.info("=============解冻金额=================");

        String userId = actionContext.getActionContext("userId").toString();
        int money = (int) actionContext.getActionContext("money");
        //解冻金额
        accountMapper.unfreezeBalance(userId,money);

        return true;
    }

    private void checkBalance(String userId, int money){
        log.info("检查用户 {} 余额", userId);
        Account account = accountMapper.selectByUserId(userId);
        
        if (account.getMoney() < money) {
            log.warn("用户 {} 余额不足,当前余额:{}", userId, account.getMoney());
            throw new RuntimeException("余额不足");
        }
        
    }
}



微服务增加tcc_fence_log日志表

# tcc_fence_log 建表语句如下(MySQL 语法)
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;



比较

一阶段连接对象操作rollback补偿机制说明
AT查询操作、业务sql执行、查询操作、往undo_log表插入数据、提交事务释放连接对象自动补偿,通过after image进行补偿需要依赖于ACID的关系型数据库;不适用于对于性能要求很高、高并发的场景
XA进行业务sql执行,预提交,一直持有连接对象自动补偿,连接对象直接rollback需要依赖于ACID、XA的关系型数据库;因为是强一致性,也不适用于性能要求高的场景
TCC在try阶段 执行业务sql、记录日志表、提交事务释放连接对象手动补偿,在cancel方法中自定义业务补偿;需要注意空回滚/幂等/悬挂。不依赖于数据库;相比较AT模式更适用于性能要求高的场景
  • 28
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值