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

news2024/11/14 4:02:22

文章目录

    • SeataXA模式
      • 整体机制
      • 微服务整合SeataXA
    • SeataTCC模式
      • 什么是TCC
      • 以用户下单为例
      • Seata TCC 模式
      • Seata TCC模式接口改造
      • TCC如何控制异常
        • 空回滚
        • 幂等
        • 悬挂
      • 微服务整合SeataTCC
    • 比较

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模式更适用于性能要求高的场景

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1932327.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

对LinkedList ,单链表和双链表的理解

一.ArrayList的缺陷 二.链表 三.链表部分相关oj面试题 四.LinkedList的模拟实现 五.LinkedList的使用 六.ArrayList和LinkedList的区别 一.ArrayList的缺陷: 1. ArrayList底层使用 数组 来存储元素&#xff0c;如果不熟悉可以来再看看&#xff1a; ArrayList与顺序表-CSDN…

zephyr BLE创建自定义服务

目录 LBS服务介绍实现过程 以创建LBS服务为例&#xff0c;在蓝牙标准里面没有这个服务&#xff0c;但是nordic有定制这个服务。 LBS服务介绍 实现过程 定义 GATT 服务及其特性的 128 位 UUID。包括服务UUID&#xff0c;特征的UUID。 #define BT_UUID_LBS_VAL BT_UUID_128_EN…

【BUG】已解决:ValueError: Expected 2D array, got 1D array instead

已解决&#xff1a;ValueError: Expected 2D array, got 1D array instead 欢迎来到英杰社区https://bbs.csdn.net/topics/617804998 欢迎来到我的主页&#xff0c;我是博主英杰&#xff0c;211科班出身&#xff0c;就职于医疗科技公司&#xff0c;热衷分享知识&#xff0c;武汉…

“论软件维护方法及其应用”精选范文,软考高级论文,系统架构设计师论文

论文真题 软件维护是指在软件交付使用后&#xff0c;直至软件被淘汰的整个时间范围内&#xff0c;为了改正错误或满足 新的需求而修改软件的活动。在软件系统运行过程中&#xff0c;软件需要维护的原因是多种多样的&#xff0c; 根据维护的原因不同&#xff0c;可以将软件维护…

【Linux】线程——线程互斥的概念、锁的概念、互斥锁的使用、死锁、可重入和线程安全、线程同步、条件变量的概念和使用

文章目录 Linux线程4. 线程互斥4.1 线程互斥的概念4.2 锁的概念4.2.1 互斥锁的概念4.2.2 互斥锁的使用4.2.3 死锁4.2.4 可重入和线程安全 5. 线程同步5.1 条件变量的概念5.2 条件变量的使用 Linux线程 4. 线程互斥 我们之前使用了线程函数实现了多线程的简单计算模拟器。 可以…

3D问界—在MAYA中使用Python脚本进行批量轴居中

问题提出&#xff1a;MAYA中如何使用Python脚本 今天不是一篇纯理论&#xff0c;主要讲一下MAYA中如何使用Python脚本&#xff0c;并解决一个实际问题&#xff0c;文章会放上我自己的代码&#xff0c;若感兴趣欢迎尝试&#xff0c;当然&#xff0c;若有问题可以见文章末尾渠道&…

防火墙--带宽管理

目录 核心思想 带宽限制 带宽保证 连接数的限制 如何实现 接口带宽 队列调度 配置位置 在接口处配置 带宽策略配置位置 带宽通道 配置地方 接口带宽、带宽策略和带宽通道联系 配置顺序 带块通道在那里配置 选项解释 引用方式 策略独占 策略共享 重标记DSCP优先…

C# 中IEnumerable与IQuerable的区别

目的 详细理清IEnumerator、IEnumerable、IQuerable三个接口之间的联系与区别 继承关系&#xff1a;IEnumerator->IEnumerable->IQuerable IEnumerator&#xff1a;枚举器 包含了枚举器含有的方法&#xff0c;谁实现了IEnuemerator接口中的方法&#xff0c;就可以自定…

【坑】微信小程序开发wx.uploadFile和wx.request的返回值格式不同

微信小程序 使用wx.request&#xff0c;返回值是json&#xff0c;如下 {code:200,msg:"更新用户基本信息成功",data:[]} 因此可以直接使用如 res.data.code获取到返回值中的code字段 但是&#xff0c;上传图片需要使用wx.uploadFile&#xff0c;返回的结果如下 …

【知识图谱】【红楼梦】

参考链接 安装、使用教程&#xff08;知乎&#xff09;&#xff1a;https://zhuanlan.zhihu.com/p/634006024Git &#xff1a;https://github.com/chizhu/KGQA_HLM 注&#xff1a;原项目为 【 重庆邮电大学&#xff0c;2018 林智敏 的毕业设计 】。【 感谢大佬的分享 】。 jav…

Web渗透:Shiro550漏洞(CVE-2016-4437)

Apache Shiro 是一个强大且易于使用的Java安全框架&#xff0c;提供了身份验证&#xff08;Authentication&#xff09;、授权&#xff08;Authorization&#xff09;、会话管理&#xff08;Session Management&#xff09;和密码学支持等功能。Apache Shiro 550反序列化漏洞&a…

set类和map类介绍和简单使用

目录 set类介绍与简单使用 set类 multiset类 map类介绍与简单使用 map类 multimap类 set类介绍与简单使用 set类是一种关联式容器&#xff0c;在数据检索时比序列式容器效率更高。本质是一个常规的二叉搜索树&#xff0c;但是为了防止出现单支树导致效率下降进行了相关优…

188数码管轮询扫描

前言 最近用到了188数码管&#xff0c;总结一下。 188数码管&#xff0c;用5个IO&#xff0c;在不借助外部驱动芯片的情况下&#xff0c;可以点亮20个灯。188数码管广泛应用于电子烟、充电器、充电宝、DVD、高级音响、工业设备控制面板、医疗器械等多个领域&#xff0c;满足不…

FPGA FIR fdatool filter designer MATLAB

位数问题 fdatool 先确定输入信号的位宽&#xff0c;比如17位在fdatool中&#xff0c;选set quantization parameters 选input/output 设置input word length 为17bit(not confirmed) fir compiler implementation 注意&#xff1a; 当设置输入位宽为16位时&#xff0c;ip核…

Java 快速入门学习 -- Day 2

Java 快速入门 Ⅱ maven&#xff08;图书管理员&#xff09;IDEA使用 maven框架 maven&#xff08;图书管理员&#xff09; maven 仓库&#xff0c;图书馆。要看书的化先从家里找&#xff08;本地仓库&#xff09;&#xff0c;本地找不到就去中央仓库或者镜像仓库找&#xff0c…

CSA笔记3-文件管理命令(补充)+vim+打包解包压缩解压缩命令

grep(-i -n -v -w) [rootxxx ~]# grep root anaconda-ks.cfg #匹配关键字所在的行 [rootxxx ~]# grep -i root anaconda-ks.cfg #-i 忽略大小写 [rootxxx ~]# grep -n root anaconda-ks.cfg #显示匹配到的行号 [rootxxx ~]# grep -v root anaconda-ks.cfg #-v 不匹配有…

记录些MySQL题集(8)

ACID原则、事务隔离级别及事务机制原理 一、事务的ACID原则 什么是事务呢&#xff1f;事务通常是由一个或一组SQL组成的&#xff0c;组成一个事务的SQL一般都是一个业务操作&#xff0c;例如聊到的下单&#xff1a;「扣库存数量、增加订单详情记录、插入物流信息」&#xff0…

AQS详解

文章目录 AQS 是什么&#xff1f;AQS 的原理是什么&#xff1f;AQS 资源共享方式总结 AQS 是什么&#xff1f; AQS 的全称为 AbstractQueuedSynchronizer &#xff0c;翻译过来的意思就是抽象队列同步器。这个类在 java.util.concurrent.locks 包下面。 AQS是一个用来构建锁和…

【操作系统】定时器(Timer)的实现

这里写目录标题 定时器一、定时器是什么二、标准库中的定时器三、实现定时器 定时器 一、定时器是什么 定时器也是软件开发中的⼀个重要组件.类似于⼀个"闹钟".达到⼀个设定的时间之后,就执行某个指定 好的代码. 定时器是⼀种实际开发中⾮常常用的组件. ⽐如⽹络通…

base SAS programming学习笔记13(Array)

1.Array array-name{dimension} <elements> array-name&#xff1a;向量名称 dimension&#xff1a;向量长度&#xff0c;默认为1&#xff1b; elements:列出变量名&#xff0c;变量名要么全是数值变量或者全是字符变量 array-name和variable不能相同&#xff1b;也不能和…