TCC事务模式使用

发布时间 2024-01-10 14:31:43作者: 周仙僧

整体机制

TCC模式采用的也是两阶段提交的模型,区别于AT和XA模式,TCC模式的两阶段需要自定义实现,不依赖于数据库的事务模型和协议。
机制示例图

工作机制

TCC模式客户端使用时需要分try、commit、cancel三个部分:

  • try:检查预留资源
  • commit:执行真正业务的提交
  • Cancel:预留资源的释放

工作机制示例图

异常场景控制

在 TCC 模型执行的过程中,还可能会出现各种异常,其中最为常见的有空回滚、幂等、悬挂等。

空回滚

出现场景:
空回滚指的是在一个分布式事务中,在没有调用参与方的 Try 方法的情况下,TM 驱动二阶段回滚调用了参与方的 Cancel 方法;一般出现原因是执行Try阶段时,try执行机器发生网络异常或者宕机。
处理方式:
新增一个 TCC 事务控制表,包含事务的 XID 和 BranchID 信息,在 Try 方法执行时插入一条记录,表示一阶段执行了,执行 Cancel 方法时读取这条记录,如果记录不存在,说明 Try 方法没有执行。

幂等

出现场景:
分支参与者在执行完commit后出现网络故障或者宕机,导致TC没有收到commit的返回结果,TC 会重复发起调用,直到二阶段执行结果成功。
处理方式:
在 TCC 事务控制表中增加一个记录状态的字段 status,该字段有 3 个值,分别为:
-tried:1
-committed:2
-rollbacked:3
二阶段 Confirm/Cancel 方法执行后,将状态改为 committed 或 rollbacked 状态。当重复调用二阶段 Confirm/Cancel 方法时,判断事务状态即可解决幂等问题。

防悬挂

出现场景:
分支参与者在执行一阶段 Try 方法时,出现网路拥堵,由于 Seata 全局事务有超时限制,执行 Try 方法超时后,TM 决议全局回滚,回滚完成后如果此时 RPC 请求才到达参与者 A,执行 Try 方法进行资源预留,从而造成悬挂。
处理方式:
当执行二阶段 Cancel 方法时,如果发现 TCC 事务控制表没有相关记录,说明二阶段 Cancel 方法优先一阶段 Try 方法执行,因此在事务控制表插入一条回滚记录;在执行一阶段Try时先读取事务控制表,若查询到回滚记录,说明出现悬挂,则停止执行业务直接返回。

集成过程

Seata客户端集成
事务执行客户端基本搭建过程与其他模式一致,全局事务发起方添加@GlobalTransactional注解即可,事务接收方需要实现自定义TTC(Try-Commit-Cancel),过程如下:

  • TCC定义
  1. 业务层接口上添加@LocalTCC注解
  2. 指定执行事务一阶段的方法,在方法上添加@TwoPhaseBusinessAction,同时指定二阶段的commit方法和cancel方法。
    参考代码
@LocalTCC
public interface FundManageService {

    List<FundInfoEntity> queryFundInfo(FundInfoDto dto);

    FundInfoEntity queryFundInfoDetail(String code);

    FundInfoEntity queryFundInfoDetailForGlobal(String code);

    void addFundInfo(FundInfoDto dto);

    /**
     * 编辑数据
     * @param dto
     */
    void editFundInfo(FundInfoDto dto);

    /**
     * 编辑数据
     * @param actionContext
     * @param dto
     */
    @TwoPhaseBusinessAction(name = "EditFundInfoOne", commitMethod = "commit", rollbackMethod = "rollback")
    void editFundInfo(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "tx_param") FundInfoDto dto);

    /**
     * 减少基金池份额
     * @param dto
     */
    void reduceFundShr(FundInfoDto dto);


    boolean commit(BusinessActionContext actionContext);

    boolean rollback(BusinessActionContext actionContext);
  • TCC实现
    1.实现一阶段try,实现内容包含防悬挂、预留业务资源、添加全局事务日志
@Override
    public void editFundInfo(BusinessActionContext actionContext, FundInfoDto dto) {
        //1. 实现防悬挂
        FundTransactionEntity transactionEntity = fundTransactionMapper.selectTxById(actionContext.getBranchId(), actionContext.getXid());
        if (null != transactionEntity) {
            logger.error("【TRY-TRY】事务已经回滚,出现悬挂,流程终止,xid:{}, branchId:{}", actionContext.getXid(), actionContext.getBranchId());
            return;
        }
        //2. 插入事务控制记录,用于后续处理幂等和空回滚
        addTxLog(dto, actionContext, FundTransactionTypeEnum.TCC_TRY.name(), FundTransactionStateEnum.STATE_TRY.name());
        //3. 冻结资金份额
        FundInfoEntity entity = new FundInfoEntity();
        BeanUtils.copyProperties(dto, entity);
        fundManageMapper.freezeFundShr(entity);
    }
  1. 实现二阶段commit,实现内容包含保证幂等、完成业务提交、修改全局事务状态
@Override
    public boolean commit(BusinessActionContext actionContext) {
        //1. 基于全局事务id和分支事务id获取事务记录
        FundTransactionEntity transactionEntity = fundTransactionMapper.selectTxById(actionContext.getBranchId(), actionContext.getXid());
        //2. 判断是否为空,如果为空,说明一阶段的try没有执行
        if (transactionEntity == null) {
            logger.error("【TRY-COMMIT】获取事务记录失败,流程终止,xid:{}, branchId:{}", actionContext.getXid(), actionContext.getBranchId());
            return true;
        } else if (transactionEntity.getState().equals(FundTransactionStateEnum.STATE_COMMIT.name())) {
            //3. 判断事务是否已经提交,若提交,流程终止,保证幂等性
            logger.error("【TRY-COMMIT】事务记录已经提交,流程终止,xid:{}, branchId:{}", actionContext.getXid(), actionContext.getBranchId());
            return true;
        } else if (transactionEntity.getState().equals(FundTransactionStateEnum.STATE_CANCEL.name())) {
            //4. 判断事务是否已经回滚,若回滚,流程终止
            logger.error("【TRY-COMMIT】事务记录已经回滚,流程终止,xid:{}, branchId:{}", actionContext.getXid(), actionContext.getBranchId());
            return true;
        }

        FundInfoDto dto = actionContext.getActionContext(TX_PARAM, FundInfoDto.class);
        FundInfoEntity entity = new FundInfoEntity();
        BeanUtils.copyProperties(dto, entity);
        //5. 执行业务代码,完成业务
        fundManageMapper.commitFundShr(entity);
        //6. 调整事务日志记录为“提交”
        Integer row = updateTxLog(actionContext, FundTransactionStateEnum.STATE_COMMIT.name());
        Assert.state(row != null && row > 0,"【TRY-COMMIT】事务记录提交失败,事务状态已经发生变化,需要更新数据!");
        return true;
    }

3.实现二阶段cancel,实现内容包含防止空回滚、释放预留资源、修改全局事务状态

@Override
    public boolean rollback(BusinessActionContext actionContext) {
        //1. 先查询事务记录,判断是否为空(空回滚)
        FundTransactionEntity transactionEntity = fundTransactionMapper.selectTxById(actionContext.getBranchId(), actionContext.getXid());
        FundInfoDto dto = actionContext.getActionContext(TX_PARAM, FundInfoDto.class);
        FundInfoEntity entity = new FundInfoEntity();
        if (dto == null) {
            return true;
        }
        BeanUtils.copyProperties(dto, entity);
        //2. 如果为空,插入一条CANCEL类型的事务记录,并终止方案
        if (transactionEntity == null) {
            logger.error("【TRY-ROLLBACK】事务记录出现空回滚,流程终止,xid:{}, branchId:{}", actionContext.getXid(), actionContext.getBranchId());
            this.addTxLog(dto, actionContext, FundTransactionTypeEnum.TCC_TRY.name(), FundTransactionStateEnum.STATE_CANCEL.name());
            //出现空回滚以后,无论是否出现异常,都应该直接结束方法
            return true;
        }

        //3. 如果不为空,判断事务状态是否已经回滚,如果回滚说明重复执行了回滚操作,直接结束方法
        if (transactionEntity.getState().equals(FundTransactionStateEnum.STATE_CANCEL.name())) {
            logger.error("【TRY-ROLLBACK】事务记录重复回滚,流程终止,xid:{}, branchId:{}", actionContext.getXid(), actionContext.getBranchId());
            return true;
        } else if (transactionEntity.getState().equals(FundTransactionStateEnum.STATE_COMMIT.name())) {
            //4. 否则判断是否为已提交,如果是,说明流程异常,结束方法
            logger.error("【TRY-ROLLBACK】事务已经提交,不允许再回滚,流程终止,xid:{}, branchId:{}", actionContext.getXid(), actionContext.getBranchId());
            return true;
        }

        //5. 如果是初始化一阶段完成状态,则回滚修改的业务数据
        fundManageMapper.unFreezeFundShr(entity);

        //6. 变更事务状态为“已回滚”
        Integer row = this.updateTxLog(actionContext, FundTransactionStateEnum.STATE_CANCEL.name());
        Assert.state(row != null && row > 0,"【TRY-ROLLBACK】事务记录提交失败,事务状态已经发生变化,需要更新数据!");
        return true;
    }

特征

  • 优点:TCC过程自定义实现,不依赖于数据库,可以解决各种复杂场景以及非关系型数据库类型的分布式事务问题。
  • 缺点: 实现TCC过程对业务代码渗透的较深,代码编码量比较大,提高了实现难度。

参考文献

深度剖析 Seata TCC 模式
TCC 理论及设计实现指南介绍