分布式事务 - Seata - TCC模式

news2024/11/16 21:23:00

目录

    • 一、什么是TCC
    • 二、AT & TCC区别 及 适用场景
    • 三、代码集成示例
      • 3.1 升级Seata 1.5.2
      • 3.2 示例场景说明
      • 3.3 TCC核心接口定义
      • 3.4 TCC相关阶段规划
      • 3.5 TCC核心代码
    • 四、TCC三大问题(幂等、空回滚、悬挂)

之前介绍过分布式事务之Seata AT模式,这篇文章接着介绍如何使用Seata TCC模式。

一、什么是TCC

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

  • Try:对业务资源的检查并预留。
    • 若分支事务的Try操作出现异常,则直接全局事务回滚,其他分支事务的Cancel被触发;
    • 若所有分支事务的Try操作均成功,则直接提交全局事务,所有分支事务的Confirm被触发;
  • Confirm:对业务处理进行提交,即 commit 操作。
    • 若Try 成功,需要保证Confirm也一定成功;
    • 若Confirm操作自身出现异常,则Confirm操作会被多次重复调用;
  • Cancel:对业务处理进行取消,即回滚操作。
    • 该步骤仅需对 Try 预留的资源进行释放;
    • 若Cancel操作自身出现异常,则Cancel操作会被多次重复调用;

二、AT & TCC区别 及 适用场景

一个分布式的全局事务,整体是两阶段提交Try-[Comfirm/Cancel] 的模型。在Seata中,AT模式与TCC模式事实上都是两阶段提交的具体实现

他们的区别在于:
AT 模式基于支持本地 ACID 事务 的 关系型数据库(目前支持Mysql、Oracle与PostgreSQL):

  • 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志undo_log记录。
    • 由Seata框架自动生成undo_log,无需开发者手动编程实现
    • 开发者仅定义核心业务逻辑,即实现一个@Transactional方法
  • 二阶段 commit 行为:马上成功结束,自动异步批量清理回滚日志。
  • 二阶段 rollback 行为:通过回滚日志,自动生成补偿操作,完成数据回滚。

TCC 模式不依赖于底层数据资源的事务支持,且Try-Confirm-Cancel三者完全由开发者自行开发定义:

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

AT和TCC模式在一阶段都会提交本地事务,二阶段都是异步执行,
相较于传统XA阻塞型事务

  • 性能更高
  • 无法做到强一致性,而是最终一致性,需要处理并且能够接受中间状态(软状态)

所谓 TCC 模式,是指支持把 自定义的 分支事务纳入到全局事务的管理中。
简单点概括,SEATA的TCC模式就是手工的AT模式,它允许你自定义两阶段的处理逻辑不依赖AT模式的undo_log

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

如果服务中仅涉及到DB(Mysql、Oracle、PostgreSQL)持久化,Seata的AT模式基本上就足够了,且AT模式除了使用@GlobalTransactional注解外几乎不侵入代码(非侵入式)。

但是当服务中涉及如下情况,则可考虑使用TCC模式

  • 不支持事务的数据库与中间件(如redis)等的操作
  • AT模式暂未支持的数据库(目前AT支持Mysql、Oracle与PostgreSQL)
  • 跨公司服务(第三方服务)的调用(无法共享Seata Server)
  • 跨语言的应用调用
  • 有手动控制整个二阶段提交过程的需求

:TCC模式对业务侵入较大,本文的选型建议是在仅考虑AT、TCC模式下给出,
实际使用时可亦可考虑其他更轻量、侵入低的分布式事务实现方式,如可靠消息、SAGA等模式。

三、代码集成示例

3.1 升级Seata 1.5.2

本示例基于Seata Server 1.5.2版本,关于Seata Server 1.5.2的升级过程可参见我之前的博客:升级Seata Server 1.5.2

且Seata1.5版本后解决了TCC模式下的幂等、空回滚、悬挂的问题,
若需支持此特性,还需在各自服务的业务数据库中额外导入表tcc-fence-log:

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;

3.2 示例场景说明

结合Seata官网下单流程示例,具体讲解Java客户端如何集成Seata TCC模式
在这里插入图片描述

示例模拟了一个下单流程,即由业务应用Business依次调用(服务间采用Openfeign Http调用)

  1. Storage -> 扣库存
  2. Order -> Account扣减用户余额
  3. Order -> 新建订单

Maven依赖、客户端Nacos配置说明可参见我之前的博客:分布式事务 - Seata - AT入门 => 三、AT模式

完整的示例代码可参见:https://gitee.com/luoex/distributed-transaction-demo/tree/master/seata-tcc
相关的Nacos配置、Sql定义可参见:https://gitee.com/luoex/distributed-transaction-demo/tree/master/config/seata

3.3 TCC核心接口定义

TCC核心就是Try-Confrim-Cancel三段逻辑的实现,在Seata中可通过定义一个接口及对应的方法来标记这三段逻辑,之后再具体实现该接口。同时由于我们使用的是 OpenFeign(基于Http协议),因此需在接口声明处使用@LocalTCC注解。TCC模式相关注解说明如下:

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

注:以上TCC相关注解仅在接口定义中进行标记即可,具体的接口实现类中无需再重复标记。

以库存服务为例,作为分支事务其TCC接口定义如下:

import com.luo.dt.common.model.result.RespResult;
import com.luo.dt.seata.tcc.model.dto.DeductStorageDto;
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;

/**
 * 库存信息 服务类<br/>
 * 注:在Tcc接口上标注@LocalTcc注解
 *
 * @author luohq
 * @since 2021-11-02
 */
@LocalTCC
public interface IStorageService {

    /**
     * 减库存
     *
     * @param deductStorageDto 扣库存参数
     * @return 响应结果
     */
    @TwoPhaseBusinessAction(
            //该tcc的bean名称,写方法名便可,全局唯一
            name = "deduct",
            //二阶段确认方法
            commitMethod = "commitDeduct",
            //二阶段取消方法
            rollbackMethod = "cancelDeduct",
            //启用tcc防护(避免幂等、空回滚、悬挂)
            useTCCFence = true
    )
    RespResult deduct(@BusinessActionContextParameter("deductStorageDto") DeductStorageDto deductStorageDto);

    /**
     * 确认方法,与@TwoPhaseBusinessAction.commitMethod对应
     * 注:context可以传递try方法的参数
     *
     * @param context 上下文
     * @return 是否成功
     */
    Boolean commitDeduct(BusinessActionContext context);

    /**
     * 回滚方法,与@TwoPhaseBusinessAction.rollbackMethod对应
     * 注:context可以传递try方法的参数
     *
     * @param context 上下文
     * @return 是否成功
     */
    Boolean cancelDeduct(BusinessActionContext context);

}

除了以上单独为TCC定义的接口(仅包含TCC三个方法),也可以在我们普通的服务接口上(包含多个业务方法)通过TCC相关注解仅对相关方法进行标记即可,如我实际示例代码中集成了Mybatis-Plus的IService接口,该接口定义了Mybatis-Plus框架内置的许多方法,我们仅需对TCC相关的业务方法进行标记即可。

interface IStorageService extends IService<Storage> { ... }

3.4 TCC相关阶段规划

TCC模式下各服务的Try-Confirm-Cancel实现内容如下:
在这里插入图片描述

注:
Order服务的Cancel阶段正常应该实现Try创建订单的补偿操作,即删除之前Try方法中保存的DB订单信息,
但由于当前Order服务位于整个服务调用链路的最后一位,执行到Order服务的Try阶段,即也意味着前面的Storage、Account服务的Try阶段都成功了,
不存在Order服务的Try阶段执行成功了还需进行全局回滚的情况,所以也就没必要实现Order服务的Cancel方法(其实就是懒😓)。
若需实现此Cancel方法,后文具体代码示例中有给出相关说明。

首先Business服务通过 @GlobalTransactional 标记的方法开启全局事务,然后依次调用Storage服务、Order服务 --> Account服务。
服务仅通过RPC协议(本例采用HTTP协议)暴露Try方法即可,且服务间的调用采用OpenFeign(Http协议)。

该编程模型需要注意的点如下:

  • TCC模式中的分支事务的Try方法组成了服务间的调用链,也就是说Try方法是通过RPC同步调用的,同步返回结果,而Confirm、Cancel都是异步调用的,无法同步返回结果。若上游服务需要下游服务的返回结果,如Business服务需要同步获取Order服务的订单创建信息,则订单创建信息需在Order服务的Try方法中返回;

  • 仅当所有分支事务的Try操作均成功,才会提交全局事务,触发所有分支事务的Confirm方法;

    • 若Try 成功,需要保证Confirm也一定成功
    • 若Confirm操作自身出现异常,则Confirm操作会被多次重复调用;
  • 若任一分支事务的Try操作出现异常,则回滚全局事务,触发 其他分支(不包括当前Try操作异常的分支) 事务的Cancel方法;

    • Cancel方法仅需对 Try 预留的资源进行释放
    • 若Cancel操作自身出现异常,则Cancel操作会被多次重复调用;
  • Try、Confirm、Cancel方法需保证自身的本地事务实现,如通过@Transactional注解修饰

    • 例如Try方法通过@Transactional注解保证本地事务实现,在Try方法出现异常时本地事务自动回滚(由于采用Tcc Fence,避免了空回滚问题则不会调用Cancel进行回滚,由Try方法自身保证本地事务)
    • 在Try方法正常结束时(本地事务已提交),若其他分支事务Try方法出现异常,则Seata全局回滚事务,调用Cancel方法进行回滚,Cancel需对Try操作预留的资源进行回滚。参考上图中的Account服务,即便用户扣款成功了,但是之后的Order服务创建订单失败了,导致全局事务回滚,那么Account服务需对之前扣减的款项进行手动回滚(增加之前扣减的金额)。
  • 业务逻辑放在Try 或 Confirm?

    • 如果上游服务依赖下游服务的返回结果,则下游服务需在try中处理返回结果
    • try中放置前置约束(如调用第三方服务、资源预留等)
      • 如果前置约束不通过,则无法继续进行后续的业务处理了,事务全局回滚后通过Cancel来对Try进行补偿,比如扣减订单金额,必须在Try阶段把钱从客户账户扣掉,如果不扣掉,在Confirm阶段客户账户钱不够了,就会出问题。
      • 仅当前置约束通过了,后续的业务逻才能确保成功,此时后续的业务逻辑可通过Confirm完成。
    • 需要考虑软状态(中间状态)。以转账为例,需要先(Try)扣款,否则用户在Confirm执行前花光了银行卡里的钱就没法再扣款了;同理不能先(Try)增款,否则在Cancel执行前将新增的金额花掉了就没法进行回滚了,总结起来就是Try扣款,Confirm增款。

3.5 TCC核心代码

Business服务开启全局事务代码如下:

/**
 * 业务服务 - 接口类
 *
 * @author luohq
 * @date 2022-12-03
 */
public interface BusinessService {

    /**
     * 创建订单
     *
     * @param businessDto 业务参数
     * @return 响应结果
     */
    RespResult<Order> handleBusinessAt(BusinessDto businessDto);

}

---------------------------------------------------------------------

/**
 * 业务服务 - 实现类
 *
 * @author luo
 * @date 2022-12-03
 */
@Service
@Slf4j
public class BusinessServiceImpl implements BusinessService {

    @Resource
    private StorageFeignClient storageFeignClient;

    @Resource
    private OrderFeignClient orderFeignClient;

    /**
     * 下单操作 - TCC全局事务通过@GlobalTransctional注解发起
     *
     * @param businessDto 业务参数
     * @return 响应结果
     */
    @Override
    @GlobalTransactional(timeoutMills = 60000 * 2)
    public RespResult<Order> handleBusinessAt(BusinessDto businessDto) {
        log.info("开始TCC全局事务,XID={}", RootContext.getXID());
        
        /** 扣减库存 */
        DeductStorageDto deductStorageDto = new DeductStorageDto(businessDto.getCommodityCode(), businessDto.getCount());
        log.info("RPC扣减库存,参数:{}", deductStorageDto);
        RespResult storageResult = this.storageFeignClient.deduct(deductStorageDto);
        log.info("RPC扣减库存,结果:{}", storageResult);
        if (!RespResult.isSuccess(storageResult)) {
            throw new MsgRuntimeException("RPC扣减库存 - 返回失败结果!");
        }

        /** 创建订单 */
        CreateOrderDto createOrderDto = new CreateOrderDto(businessDto.getUserId(), businessDto.getCommodityCode(), businessDto.getCount());
        log.info("RPC创建订单,参数:{}", createOrderDto);
        RespResult<Order> orderResult = this.orderFeignClient.createOrder(createOrderDto);
        log.info("RPC创建订单,结果:{}", orderResult);
        if (!RespResult.isSuccess(orderResult)) {
            throw new MsgRuntimeException("RPC创建订单 - 返回失败结果!");
        }
        return orderResult;
    }

}

注:
若全局事务发起者除了发起服务RPC调用,也需要实现自身对应的分支事务TCC处理逻辑,
则可单独定义并实现TCC接口,然后在@GlobalTransactional方法中调用该TCC接口即可,
且在Seata中支持TCC模式和AT模式的混合使用。


Storage服务TCC事务核心实现代码如下:

/**
 * 库存信息 服务类<br/>
 * 注:在Tcc接口上标注@LocalTcc注解
 *
 * @author luohq
 * @since 2022-12-03
 */
@LocalTCC
public interface IStorageService extends IService<Storage> {

    /**
     * 减库存
     *
     * @param deductStorageDto 扣库存参数
     * @return 响应结果
     */
    @TwoPhaseBusinessAction(
            //该tcc的bean名称,写方法名便可,全局唯一
            name = "deduct",
            //二阶段确认方法
            commitMethod = "commitDeduct",
            //二阶段取消方法
            rollbackMethod = "cancelDeduct",
            //启用tcc防护(避免幂等、空回滚、悬挂)
            useTCCFence = true
    )
    RespResult deduct(@BusinessActionContextParameter("deductStorageDto") DeductStorageDto deductStorageDto);

    /**
     * 确认方法,与@TwoPhaseBusinessAction.commitMethod对应
     * 注:context可以传递try方法的参数
     *
     * @param context 上下文
     * @return 是否成功
     */
    Boolean commitDeduct(BusinessActionContext context);

    /**
     * 回滚方法,与@TwoPhaseBusinessAction.rollbackMethod对应
     * 注:context可以传递try方法的参数
     *
     * @param context 上下文
     * @return 是否成功
     */
    Boolean cancelDeduct(BusinessActionContext context);

}

----------------------------------------------------------------


/**
 * 库存信息 服务实现类
 *
 * @author luohq
 * @since 2022-12-03
 */
@Service
@Slf4j
public class StorageServiceImpl extends ServiceImpl<StorageMapper, Storage> implements IStorageService {


    private final String CACHE_STORAGE_KEY_FORMAT = "storage:%s";
    @Resource
    private RedisTemplate redisTemplate;

    /**
     * 分支事务仅使用普通@Transactional注解即可
     */
    @Override
    public RespResult deduct(DeductStorageDto deductStorageDto) {
        log.info("开始TCC分支事务,XID={}", RootContext.getXID());
        log.info("扣减商品库存,参数: {}", deductStorageDto);
        /** 模拟回滚异常 */
        if ("product-2".equals(deductStorageDto.getCommodityCode())) {
            throw new MsgRuntimeException("异常:模拟业务异常:Storage branch exception");
        }

        /** 扣减缓存中的商品库存 */
        String cacheKey = String.format(CACHE_STORAGE_KEY_FORMAT, deductStorageDto.getCommodityCode());
        Long cacheStorageCount = this.redisTemplate.opsForValue().decrement(cacheKey, deductStorageDto.getCount());
        log.info("扣减后的商品库存: {}={}", cacheKey, cacheStorageCount);
        if (cacheStorageCount < 0) {
            throw new MsgRuntimeException("扣减商品缓存库存失败!");
        }
        return RespResult.success();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean commitDeduct(BusinessActionContext context) {
        //获取事务上下文传递的参数
        DeductStorageDto deductStorageDto = context.getActionContext("deductStorageDto", DeductStorageDto.class);
        log.info("TCC提交成功, XID={}, deductStorageDto={}", context.getXid(), deductStorageDto);
        /** 扣减DB中的商品库存 */
        Integer retCount = this.baseMapper.deductStorage(deductStorageDto.getCommodityCode(), deductStorageDto.getCount());
        log.info("修改商品库存,结果: {}", retCount);
        //修改商品库存失败,则直接回滚
        if (0 >= retCount) {
            throw new MsgRuntimeException("修改商品库存失败!");
        }
        return true;
    }

    @Override
    public Boolean cancelDeduct(BusinessActionContext context) {
        //获取事务上下文传递的参数
        DeductStorageDto deductStorageDto = context.getActionContext("deductStorageDto", DeductStorageDto.class);
        log.warn("TCC回滚业务, XID={}, deductStorageDto={}", context.getXid(), deductStorageDto);
        /** 还原缓存中的商品库存 */
        String cacheKey = String.format(CACHE_STORAGE_KEY_FORMAT, deductStorageDto.getCommodityCode());
        Long cacheStorageCount = this.redisTemplate.opsForValue().increment(cacheKey, deductStorageDto.getCount());
        log.info("还原后的商品库存: {}={}", cacheKey, cacheStorageCount);
        return true;
    }
}

Order服务TCC事务核心实现代码如下:

/**
 * 订单信息 服务类<br/>
 * 注:在Tcc接口上标注@LocalTcc注解
 *
 * @author luohq
 * @date 2022-12-12
 */
@LocalTCC
public interface IOrderService extends IService<Order> {

    /**
     * 创建订单(用户扣款、创建订单)
     *
     * @param createOrderDto 创建订单参数
     * @return 响应结果
     */
    @TwoPhaseBusinessAction(
            //该tcc的bean名称,写方法名便可,全局唯一
            name = "create",
            //二阶段确认方法
            commitMethod = "commitCreate",
            //二阶段取消方法
            rollbackMethod = "cancelCreate",
            //启用tcc防护(避免幂等、空回滚、悬挂)
            useTCCFence = true
    )
    RespResult create(@BusinessActionContextParameter("createOrderDto") CreateOrderDto createOrderDto);

    /**
     * 确认方法,与@TwoPhaseBusinessAction.commitMethod对应
     * 注:context可以传递try方法的参数
     *
     * @param context 上下文
     * @return 是否成功
     */
    Boolean commitCreate(BusinessActionContext context);

    /**
     * 回滚方法,与@TwoPhaseBusinessAction.rollbackMethod对应
     * 注:context可以传递try方法的参数
     *
     * @param context 上下文
     * @return 是否成功
     */
    Boolean cancelCreate(BusinessActionContext context);
}

---------------------------------------------------------------

/**
 * 订单信息 服务实现类
 *
 * @author luohq
 * @date 2022-12-12
 */
@Service
@Slf4j
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements IOrderService {


    @Resource
    private AccountFeignClient accountFeignClient;


    /**
     * 分支事务仅使用普通@Transactional注解即可
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public RespResult<Order> create(CreateOrderDto createOrderDto) {
        log.info("开始TCC分支事务,XID={}", RootContext.getXID());

        //计算订单金额(假设商品单价5元)
        BigDecimal orderMoney = new BigDecimal(createOrderDto.getCount()).multiply(new BigDecimal(5));
        /** 用户扣款 */
        RespResult respResult = accountFeignClient.debit(new DebitDto(createOrderDto.getUserId(), orderMoney));
        log.info("RPC用户扣减余额服务,结果:{}", respResult);
        if (!RespResult.isSuccess(respResult)) {
            throw new MsgRuntimeException("RPC用户扣减余额服务失败!");
        }

        /** 创建订单 */
        Order order = new Order();
        order.setUserId(createOrderDto.getUserId());
        order.setCommodityCode(createOrderDto.getCommodityCode());
        order.setCount(createOrderDto.getCount());
        order.setMoney(orderMoney);
        log.info("保存订单信息,参数:{}", order);
        Boolean result = this.save(order);
        log.info("保存订单信息,结果:{}", result);
        if (!Boolean.TRUE.equals(result)) {
            throw new MsgRuntimeException("保存新订单信息失败!");
        }

        if ("product-3".equals(createOrderDto.getCommodityCode())) {
            throw new MsgRuntimeException("异常:模拟业务异常:Order branch exception");
        }
        return RespResult.successData(order);
    }

    @Override
    public Boolean commitCreate(BusinessActionContext context) {
        log.info("TCC提交成功, XID={}, createOrderDto={}", context.getXid(), context.getActionContext("createOrderDto"));
        return true;
    }

    @Override
    public Boolean cancelCreate(BusinessActionContext context) {
        log.warn("TCC回滚业务, XID={}, createOrderDto={}", context.getXid(), context.getActionContext("createOrderDto"));
        //此处正常应该实现create创建订单的补偿方法,即删除之前create方法中保存的DB订单信息,
        //但由于当前Order服务位于整个服务调用链路的最后一位,执行到Order服务的Try阶段,即也意味着前面的Storage、Account服务的Try阶段都成功了,
        //不存在Order服务的Try阶段执行成功了还需进行全局回滚的情况,所以也就没必要实现Order服务的Cancel方法(其实就是懒-_-|||)。

        //若需实现create创建订单的补偿方法,可在DB中新创建个表,如order_tx_relation(order_id, tx_id)
        //然后在create方法保存order成功后,同时将 (新生成的order_id, 全局事务XID) 的绑定关系保存到order_tx_relation表,
        //在此Cancel方法中通过XID查询到对应的order_id,然后删除此order_id对应的订单信息。
        return true;
    }
}

注1:
Order服务的Cancel阶段正常应该实现Try创建订单的补偿操作,即删除之前Try方法中保存的DB订单信息,
但由于当前Order服务位于整个服务调用链路的最后一位,执行到Order服务的Try阶段,即也意味着前面的Storage、Account服务的Try阶段都成功了,
不存在Order服务的Try阶段执行成功了还需进行全局回滚的情况,所以也就没必要实现Order服务的Cancel方法(其实就是懒😓)。

注2:
若需实现create创建订单的补偿方法,可在DB中新创建个表,如order_tx_relation(order_id, tx_id)
然后在create方法保存order成功后,同时将 (新生成的order_id, 全局事务XID) 的绑定关系保存到order_tx_relation表,
在此Cancel方法中通过XID查询到对应的order_id,然后删除此order_id对应的订单信息。


Account服务TCC事务核心实现代码如下:

**
 * 用户信息 服务类<br/>
 * 注:在Tcc接口上标注@LocalTcc注解
 *
 * @author luohq
 * @date 2022-12-12
 */
@LocalTCC
public interface IAccountService extends IService<Account> {

    /**
     * 用户扣款
     *
     * @param debitDto 扣款参数
     * @return 返回结果
     */
    @TwoPhaseBusinessAction(
            //该tcc的bean名称,写方法名便可,全局唯一
            name = "debit",
            //二阶段确认方法
            commitMethod = "commitDebit",
            //二阶段取消方法
            rollbackMethod = "cancelDebit",
            //启用tcc防护(避免幂等、空回滚、悬挂)
            useTCCFence = true
    )
    RespResult debit(@BusinessActionContextParameter("debitDto") DebitDto debitDto);

    /**
     * 确认方法,与@TwoPhaseBusinessAction.commitMethod对应
     * 注:context可以传递try方法的参数
     *
     * @param context 上下文
     * @return 是否成功
     */
    Boolean commitDebit(BusinessActionContext context);

    /**
     * 回滚方法,与@TwoPhaseBusinessAction.rollbackMethod对应
     * 注:context可以传递try方法的参数
     *
     * @param context 上下文
     * @return 是否成功
     */
    Boolean cancelDebit(BusinessActionContext context);
}

-----------------------------------------------------------------------------------------

/**
 * 用户信息 服务实现类
 *
 * @author luohq
 * @date 2022-12-12
 */
@Service
@Slf4j
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements IAccountService {


    /**
     * 分支事务仅使用普通@Transactional注解即可
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public RespResult debit(DebitDto debitDto) {
        log.info("开始TCC分支事务,XID={}", RootContext.getXID());
        log.info("用户扣款,参数:{}", debitDto);
        int retCount = this.baseMapper.debit(debitDto.getUserId(), debitDto.getMoney());
        log.info("用户扣款,结果:{}", retCount);
        if (0 >= retCount) {
            throw new MsgRuntimeException("用户扣款失败!");
        }
        return RespResult.success();
    }

    @Override
    public Boolean commitDebit(BusinessActionContext context) {
        log.info("TCC提交成功, XID={}, debitDto={}", context.getXid(), context.getActionContext("debitDto"));
        return true;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean cancelDebit(BusinessActionContext context) {
        DebitDto debitDto = context.getActionContext("debitDto", DebitDto.class);
        log.warn("TCC回滚业务, XID={}, debitDto={}", context.getXid(), debitDto);
        log.info("用户补款,参数:{}", debitDto);
        int retCount = this.baseMapper.debit(debitDto.getUserId(), debitDto.getMoney().multiply(new BigDecimal(-1)));
        log.info("用户补款,结果:{}", retCount);
        return true;
    }
}

四、TCC三大问题(幂等、空回滚、悬挂)

TCC 模式中存在的三大问题:幂等、空回滚、悬挂。

幂等(Confirm/Cancel方法可能被多次调用)
在 Confirm/Cancel 阶段,因为 TC 没有收到分支事务的响应,需要进行重试,这就要分支事务Try/Cancel方法支持幂等。

空回滚(Try方法没被执行,却触发了Cancel方法被执行)
在 Try 阶段,分支事务所在节点发生了故障,Try 阶段在不考虑重试的情况下,全局事务必须要走向结束状态(全局事务回滚),这个时候其实是没有执行 Try方法,当故障节点恢复后,分布式事务进行回滚则会调用二阶段的 Cancel 方法,从而形成空回滚。

悬挂(Cancel方法优先于Try方法执行)
悬挂是指因为网络问题,RM 开始没有收到Try指令,但是执行了Cancel方法后 RM 又收到了 Try 指令并且预留资源成功,这时全局事务已经结束,最终导致预留的资源不能释放。

在 Seata1.5.1 版本中,增加了一张事务控制表tcc_fence_log,该表就是来解决这个问题。而在之前 3.3 TCC核心接口定义一章中 @TwoPhaseBusinessAction 注解中的属性 useTCCFence 就是来指定是否开启这个机制,useTCCFence属性值默认是 false(不开启)。若需开启TCC防护则需指定useTCCFence=true,同时在分支事务所在服务的DB中导入如下tcc_fence_log表

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;

TCC Fence相关的详细代码可参见:

io.seata.rm.tcc.TCCResourceManager
io.seata.rm.tcc.TCCFenceHandler

大体实现思路如下:

注: 分支事务生命周期内xid、branch_id是不变且全局唯一的,其中的action_name即对应@TwoPhaseBusinessAction.name属性。

Try阶段

  • 插入tcc_fence_log(xid, branch_id, action_name, status_tried:1)
  • xid、branch_id主键唯一性保证Try阶段幂等性、避免悬挂
  • 插入成功则执行Try方法

Confirm阶段

  • 根据xid, branch_id查询tcc_fence_log记录
  • 若记录存在且状态为status_tried:1,则修改状态为status_commited:2并执行Confirm方法
  • 若记录状态已经为status_commited:2说明已经执行过Confirm方法,直接返回成功,不再重复调用Confirm方法,保证了Confirm阶段的幂等性

Cancel阶段

  • 根据xid, branch_id查询tcc_fence_log记录
  • 若记录不存在(Cancel方法先于Try方法被执行),则插入tcc_fence_log(xid, branch_id, action_name, status_suspended:4) ,同时无需调用Cancel方法直接返回,避免了Cancel阶段空回滚
    • 后续若再执行Try方法插入tcc_fence_log时会报DuplicateKeyException,避免发生悬挂
  • 若记录存在且状态为status_tried:1,则执行Cancel方法且修改状态为status_rollbacked:3
  • 若记录存在且状态已经为status_rollbacked:3 或 status_suspended:4说明已经执行过Cancel方法,直接返回成功,不再重复调用Cancel方法,证了Cancel阶段的幂等性

参考:
http://seata.io/zh-cn/blog/integrate-seata-tcc-mode-with-spring-cloud.html
http://seata.io/zh-cn/blog/seata-tcc-fence.html
http://seata.io/zh-cn/blog/seata-tcc.html

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

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

相关文章

智慧新零售异业联盟帮你搞定多店跨界整合,让你开启共富时代

大家好&#xff0c;我是林工&#xff0c;在如今的时代&#xff0c;不管你所看到的商业模式是什么样的&#xff0c;以不变应万变&#xff0c;目的只有一个&#xff0c;把买卖发展壮大&#xff0c;想要将买卖发展壮大&#xff0c;那就需要精准的吸粉引流获客方式和能力。 林工常常…

Fluent Mybatis 牛逼!

这玩意比ThreadLocal叼多了&#xff0c;吓得我赶紧分享出来。 推荐一些chrome浏览器必装的插件&#xff01; 40 个 SpringBoot 常用注解 VSCode 花式玩法&#xff08;摸鱼&#xff09;收藏一下 &#xff01; 使用fluent mybatis可以不用写具体的xml文件&#xff0c;通过jav…

【面试大全】互联网面试软实力【必看】

软实力 推荐大家使用Markdown语法写简历&#xff0c;然后再将Markdown格式转换为PDF格式后进行简历投递。如果你对Markdown语法不太了解的话&#xff0c;可以花半个小时简单看一下Markdown语法说明: http://www.markdown.cn/ 面试 假如你是网络申请者【拉勾、boss等】&…

Latex 分式格式处理

分式 分式命令&#xff1a;\frac{分子}{分母}。 对于行内短分式&#xff0c;可用斜线/输入&#xff0c;例如&#xff1a;(xy)/2 举例&#xff1a; 行内分式 \(\frac{xy}{yz} \) (显示为&#xff1a;) 行间分式 \[\frac{xy}{yz}\] (显示为&a…

【PCB设计特别篇之阻抗计算】还在用SI9000进行阻抗计算?

有了叠层信息后&#xff0c;才可以进行阻抗计算&#xff0c;走线阻抗与线宽、线距、介质厚度、绿油厚度、介质介电常数、铜箔厚度等一些信息相关&#xff0c;所以要现有叠层&#xff0c;才能进行阻抗计算。 阻抗计算的工具有很多&#xff0c;下面介绍几种&#xff1a; 1、SI900…

RSS Can:使用 Golang 实现更好的 RSS Hub 服务(一)

聊聊之前做过的一个小东西的踩坑历程&#xff0c;如果你也想高效获取信息&#xff0c;或许这个系列的内容会对你有用。 这个事情涉及的东西比较多&#xff0c;所以我考虑拆成一个系列来聊&#xff0c;每篇的内容不要太长&#xff0c;整理负担和阅读负担都轻一些。 本篇是第一…

Go代码审计学习(二)

文章目录环境搭建漏洞一&#xff1a;代码逻辑错误、没有做有效的鉴权漏洞二&#xff1a;目录穿越、任意文件读取漏洞三&#xff1a;条件竞争漏洞四&#xff1a;钩子函数执行命令参考链接环境搭建 Gitea是从gogs衍生出的一个开源项目&#xff0c;是一个类似于Github、Gitlab的多…

自学python第三年才懂,这事儿影响着最终的学习结果

前言 如果这件事你还没搞明白&#xff0c;那你学python会跟我一样&#xff0c;学了几年跟没学差不多&#xff01; 当然&#xff0c;这件事仅限于学python是想赚钱或者提升职场竞争力的人明白即可&#xff0c;其他人别浪费时间了。 这事儿很重要 我从2018年底开始自学python&a…

SpringBoot2核心技术(基础入门)- 03 自动配置原理【3.1依赖管理+2容器功能】

1、SpringBoot特点 1.1、依赖管理 ● 父项目做依赖管理 依赖管理 <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.4.RELEASE</version> </parent…

(已更新)2023春节倒计时新款HTML单页源码

2023春节倒计时新款HTML单页自适应页面&#xff0c;有兴趣的可以看看&#xff01;背景图片自己修改喜欢的&#xff01; 源码介绍 自适应页面&#xff0c;支持安卓和ioswx背景音乐播放附带多个背景音乐&#xff0c;由于技术原因&#xff0c;一些js进行了加密&#xff08;支持i…

Spring boot 使用@DS 配合druid连接池切换数据源及切换数据源失效情况

一、导入包 <!-- dynamic-datasource --> <dependency><groupId>com.baomidou</groupId><artifactId>dynamic-datasource-spring-boot-starter</artifactId><version>3.2.1</version> </dependency> 二、配置yam配置文…

【Flask框架】——09 视图和URL总结

目录一、创建flask项目1、安装环境2、创建项目3、启动程序4、访问项目二、flask参数说明1.初始化flask项目2.参数说明3.DEBUG调试模式三、应用程序的参数加载1.从配置对象中加载2.从配置文件中加载3.从环境变量中加载4.从Pycharm中的运行时设置环境变量的方式加载5.企业项目开发…

Flink-多流转换(合流,分流,双流join)

8 多流转换 8.1 分流 简单实现 对流三次filter算子操作实现分流 // 筛选 Mary 的浏览行为放入 MaryStream 流中DataStream<Event> MaryStream stream.filter(new FilterFunction<Event>(){Overridepublic boolean filter(Event value) throws Exception {retur…

vue-admin-template侧边栏修改成抽屉式

目录 一、修改sidebar组件 二、修改store 三、修改sidebaritem页面 四、修改navbar页面 五、修改layout 六、修改样式 一、修改sidebar组件 src—layout—components—sidebar—index.vue 把组件sidebar改成drawer <template><div :class"{ has-logo: s…

【AGC】崩溃服务flutter报缺失recordFatalException方法的问题

问题背景&#xff1a; cp反馈集成AGC-崩溃服务的flutter插件&#xff0c;使用最新的1.3.0300版本&#xff0c;出现下面错误 /Users/yin/Documents/projects/flutter/.pub-cache/hosted/pub.dartlang.org/agconnect_crash-1.3.0300/android/src/main/java/com/huawei/agconnec…

【脚本项目源码】Python制作艺术签名生成器,打造专属你的个人艺术签名

前言 本文给大家分享的是如何通过利用Python制作艺术签名生成器&#xff0c;废话不多直接开整~ 开发工具 Python版本&#xff1a; 3.6 相关模块&#xff1a; requests模块 PIL模块 PyQt5模块 环境搭建 安装Python并添加到环境变量&#xff0c;pip安装需要的相关模块即…

一个.NET的轻量级JWT库

这两天网上闲逛的时候&#xff0c;看到一个.NET的轻量级JWT库LitJWT&#xff0c;LitJWT号称主要关注性能&#xff0c;能提升至少5倍的编码/解码速度&#xff0c;以及更少的配置&#xff01; LitJWT支持的平台为netstandard 2.1或net5.0更高。 LitJWT宣传的性能对比图&#xf…

vulnhub靶机:presidential1

目录 进行靶机ip的扫描 nmap扫描开发的端口和服务信息 目录扫描 修改host文件 子域名搜集 phpmyadmin管理端登录 phpmyadmin漏洞利用 反弹shell capabilities提权 获取root权限 靶机总结 靶机下载网址&#xff1a;Presidential: 1 ~ VulnHub Kali ip&#xff1a;19…

数据分析方法-五大理论、分析框架、应用、数据分析作用

1、统计学理论 1.1 大数定量 定义&#xff1a; 指大量重复某一实验时&#xff0c;最后的频率会无限接近于事件的概率 数据的样本量越大&#xff0c;我们预测和计算的概率就越准确 数据的样本量越小&#xff0c;我们预测和计算的概率就越可能失效 举例&#xff1a; 某产品用户…

keepalived 主备使用

keepalived 主备使用 本篇主要介绍一下 keepalived 的基本的 主备使用 1.概述 什么是 keepalived呢,它是一个集群管理中 保证集群高可用的软件,防止单点故障,keepalived是以VRRP协议为实现基础的&#xff0c;VRRP全称Virtual Router Redundancy Protocol&#xff0c;即虚拟路冗…