Seata — TCC模式详细分析

​ 深入分析了 Seata 的 TCC 模式,通过实际使用案例引出了关键注解的应用。结合 TM、RM 和 TC 的工作流程,剖析了 TccActionInterceptor 在 try 阶段的实现原理,详细讲解了 TCCResourceManager 如何处理 commit 和 rollback 操作。同时,解析了 TCC_FENCE_LOG 表如何保障事务幂等性,并有效解决了悬挂和空回滚等常见问题

使用案例

​ 一个简单的使用TCC模式下订单,在本地扣减库存后再调用其他微服务的案例

扣库存


@LocalTCC
public interface StockTccAction {

    /**
     * 预扣库存
     */
    @TwoPhaseBusinessAction(name = "stockAction", useTCCFence = true)
    void prepareReduceInventory(@BusinessActionContextParameter(paramName = "sku") String sku,
                                @BusinessActionContextParameter(paramName = "quantity") Integer quantity);

    void commit(BusinessActionContext businessActionContext);

    void rollback(BusinessActionContext businessActionContext);

}

下单

@Component
public class OrderService {

    @Autowired
    private StockTccAction stockTccAction;

    @GlobalTransactional(rollbackFor = Exception.class)
    public void placeOrder(OrderDTO orderDTO) {

        stockTccAction.prepareReduceInventory(orderDTO.getSku(), orderDTO.getQuantity());
        // 调用其他微服务....

    }

}

TCC实现

​ Seata还是利用了GlobalTransactionScanner来实现TCC的代理。具体来说,方法上如果使用了@TwoPhaseBusinessAction注解,并且是远程引用bean(如sofa:referencedubbo:reference),或者是带有@LocalTCC类注解的方法,都会由TccActionInterceptor作为切面来实现TCC在try阶段的代理

开启全局事务

​ 首先TCC模式还是使用了@GlobalTransactional开启全局事务,这部分和AT模式是一样的,具体解析可以看之前的文章

try

​ 调用带有@TwoPhaseBusinessAction注解的TccAction方法时,会进入TccActionInterceptor切面,该切面即是TCC的try阶段。核心的业务逻辑在ActionInterceptorHandler#proceed方法中实现,下面是重点流程的总结:

  1. 构建BusinessActionContext,并填充相关参数

  2. 使用TCC的RM(TCCResourceManager)向TC注册分支事务

    • 注册时,BusinessActionContext的所有信息会被封装在BranchRegisterRequest中,并存储在BranchTransactionDO里。之后在commitrollback阶段时会传回这些数据
    • TC处理TCC分支事务注册的逻辑和AT注册分支事务的逻辑是一套的,唯一的区别是TCC不需要获取锁资源(BranchSession#lock方法只对AT生效)
  3. 根据useTCCFence决定try阶段业务逻辑的执行方式

    • useTCCFence=true:在事务的try阶段,会先插入一条状态为STATUS_TRIEDTCC_FENCE_LOG数据,并随后执行业务代码。这两个操作会在同一个本地事务中执行
    • useTCCFence=false:则直接执行业务代码
public Object proceed(Method method, Object[] arguments, String xid, TwoPhaseBusinessAction businessAction,
        Callback<Object> targetCallback) throws Throwable {
    // 获取参数里的BusinessActionContext,没有就新建
    BusinessActionContext actionContext = getOrCreateActionContextAndResetToArguments(method.getParameterTypes(),
            arguments);

    actionContext.setXid(xid);
    String actionName = businessAction.name();
    actionContext.setActionName(actionName);
    actionContext.setDelayReport(businessAction.isDelayReport());

    // 注册TCC分支事务到TC
    String branchId = doTccActionLogStore(method, arguments, businessAction, actionContext);
    actionContext.setBranchId(branchId);
    MDC.put(RootContext.MDC_KEY_BRANCH_ID, branchId);

    // TCC使用存在嵌套,先将外面的TCC拿出来
    BusinessActionContext previousActionContext = BusinessActionContextUtil.getContext();
    try {
        // 绑定当前事务上下文
        BusinessActionContextUtil.setContext(actionContext);

        // 如果启用了 TCC Fence,则使用 TCC Fence 机制进行处理
        if (businessAction.useTCCFence()) {
            try {
                // 插入一条状态为STATUS_TRIED的tcc_fence_log,再执行业务代码(这两个操作会在一个本地数据库事务中执行)
                return TCCFenceHandler.prepareFence(xid, Long.valueOf(branchId), actionName, targetCallback);
            } catch (SkipCallbackWrapperException | UndeclaredThrowableException e) {
                Throwable originException = e.getCause();
                if (originException instanceof FrameworkException) {
                    LOGGER.error("[{}] prepare TCC fence error: {}", xid, originException.getMessage());
                }
                throw originException;
            }
        } else {
            return targetCallback.execute();
        }
    } finally {
        try {
            // 向TC报告
            BusinessActionContextUtil.reportContext(actionContext);
        } finally {
            if (previousActionContext != null) { // 恢复上一个事务上下文
                BusinessActionContextUtil.setContext(previousActionContext);
            } else { // 清空当前的事务上下文
                BusinessActionContextUtil.clear();
            }
        }
    }
}

commit

​ 当 @GlobalTransactional 注解的全局事务执行完毕并准备提交时,服务端的处理方式和前面的AT模式TC处理commit逻辑基本一致,只有点小区别:

  • TCC模式没有锁资源,不需要释放
  • 整个提交过程是同步的,TC会在提交阶段同步的回调RM来处理分支事务的提交

​ 而客户端的RM处理分支事务核心逻辑在 TCCResourceManager#branchCommit 方法中,主要逻辑总结如下:

  1. 获取 TCCResource 对象

    ​ 据 @TwoPhaseBusinessAction 注解中的 name 属性,获取对应的 TCCResource 对象,这个对象代表了当前需要提交的 TCC 资源

  2. 构建 BusinessActionContext

    使用 BranchTransactionDO 填充 BusinessActionContext,该上下文会在整个 TCC 流程中传递,确保业务信息一致性

  3. 判断是否使用 TCC Fence

    • useTCCFence=true:一些列对TCC_FENCE_LOG的校验后再修改其状态为STATUS_COMMITTED,执行业务提交操作
    • useTCCFence=false:直接执行业务提交操作
public BranchStatus branchCommit(BranchType branchType, String xid, long branchId, String resourceId,
        String applicationData) throws TransactionException {
    // 从缓存中获取TCC资源对象
    // 早在对bean做能否被TCC代理判断时就已经缓存了,方法在DefaultRemotingParser#parserRemotingServiceInfo里
    // resourceId即为@TwoPhaseBusinessAction.name属性
    TCCResource tccResource = (TCCResource) tccResourceCache.get(resourceId);
    if (tccResource == null) {
        throw new ShouldNeverHappenException(String.format("TCC resource is not exist, resourceId: %s", resourceId));
    }
    // 获取目标TCC Bean和commit方法
    Object targetTCCBean = tccResource.getTargetBean();
    Method commitMethod = tccResource.getCommitMethod();
    if (targetTCCBean == null || commitMethod == null) {
        throw new ShouldNeverHappenException(
                String.format("TCC resource is not available, resourceId: %s", resourceId));
    }
    try {
        // 构建BusinessActionContext
        BusinessActionContext businessActionContext = getBusinessActionContext(xid, branchId, resourceId,
                applicationData);
        Object[] args = this.getTwoPhaseCommitArgs(tccResource, businessActionContext);
        Object ret;
        boolean result;
        if (Boolean.TRUE.equals(businessActionContext.getActionContext(Constants.USE_TCC_FENCE))) {
            try {
                // 执行TCC Fence的提交操作
                result = TCCFenceHandler.commitFence(commitMethod, targetTCCBean, xid, branchId, args);
            } catch (SkipCallbackWrapperException | UndeclaredThrowableException e) {
                throw e.getCause();
            }
        } else {
            ret = commitMethod.invoke(targetTCCBean, args);
            if (ret != null) {
                if (ret instanceof TwoPhaseResult) {
                    result = ((TwoPhaseResult) ret).isSuccess();
                } else {
                    result = (boolean) ret;
                }
            } else {
                result = true;
            }
        }
        return result ? BranchStatus.PhaseTwo_Committed : BranchStatus.PhaseTwo_CommitFailed_Retryable;
    } catch (Throwable t) { // 允许TC重试
        return BranchStatus.PhaseTwo_CommitFailed_Retryable;
    }
}

cancel

​ TCC 模式下,全局事务回滚时触发的操作与提交(commit)类似,唯一不同的是最终执行的业务方法变为 rollbackMethod,其他流程与提交过程一致,就不细讲了

常见问题解决

​ 在前文的源码分析中,我们简要提到过 tcc_fence_log 表。这里将详细解析该表的作用,它是 Seata 用来解决 TCC 模式中可能出现的悬挂、空回滚等问题,并确保整个流程的幂等性。每个分支事务仅会对应一条 tcc_fence_log 数据(xidbranch_id 作为主键)。对该数据的判断与操作都通过本地数据库事务进行,从而将其与业务代码绑定,所有逻辑封装在 TCCFenceHandler 类中

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_log 状态的校验,能够保证事务的幂等性。这是通过检查 tcc_fence_log 中的状态是否已被更新为 COMMITTED 或 **ROLLBACKED**,从而避免重复执行相同的操作。整个过程相对简单,确保了事务的幂等性,无需过多分析

空回滚

背景

​ 分支事务的 try 阶段出现异常或超时,导致 全局事务 发起回滚操作。此时,TC 会调用 RM 来触发分支事务的回滚操作。然而,分支事务本身并未成功进入 try 阶段,因此实际上不需要执行回滚。若此时仍然进行回滚操作,就会导致空回滚

解决方案

​ 当 cancel 时,如果 tccFenceDO 记录为空,表示 try 阶段未成功执行。在这种情况下,尝试插入一条状态为 SUSPENDEDtcc_fence_log 记录,以阻止后续的 try (悬挂)执行。然后直接返回 true表示回滚成功,避免执行 rollback 方法

public static boolean rollbackFence(Method rollbackMethod, Object targetTCCBean,
        String xid, Long branchId, Object[] args, String actionName) {
    return Boolean.TRUE.equals(transactionTemplate.execute(status -> {
        try {
            Connection conn = DataSourceUtils.getConnection(dataSource);
            TCCFenceDO tccFenceDO = TCC_FENCE_DAO.queryTCCFenceDO(conn, xid, branchId);
            if (tccFenceDO == null) { // 空回滚场景,try都没有执行成功
                // 插入一条状态为SUSPENDED的tcc_fence_log记录,这样就算TCC中的cancel先于try执行,也不用担心try会被触发
                boolean result = insertTCCFenceLog(conn, xid, branchId, actionName, TCCFenceConstant.STATUS_SUSPENDED);
                if (!result) {
                    throw new TCCFenceException(String.format(
                            "Insert tcc fence record error, rollback fence method failed. xid= %s, branchId= %s", xid,
                            branchId),
                            FrameworkErrorCode.InsertRecordError);
                }
                return true;
            } else { // 幂等保证
                if (TCCFenceConstant.STATUS_ROLLBACKED == tccFenceDO.getStatus()
                        || TCCFenceConstant.STATUS_SUSPENDED == tccFenceDO.getStatus()) {
                    return true;
                }
                if (TCCFenceConstant.STATUS_COMMITTED == tccFenceDO.getStatus()) {
                    return false;
                }
            }
            return updateStatusAndInvokeTargetMethod(conn, rollbackMethod, targetTCCBean, xid, branchId,
                    TCCFenceConstant.STATUS_ROLLBACKED, status, args);
        } catch (Throwable t) {
            status.setRollbackOnly();
            throw new SkipCallbackWrapperException(t);
        }
    }));
}

悬挂

背景

​ 分支事务的 try 阶段正常执行,但由于网络或其他问题,分支事务进入了等待状态,导致 全局事务 超时并触发了回滚操作。此时,TC 会调用 RM 进行分支事务的回滚。然而,在回滚操作完成后,分支事务的 try 阶段最终才成功执行

解决方案

​ 在 try 阶段,首先尝试插入一条 TCCFenceLog 记录(主键确保每个分支事务仅会有一条记录)。如果此时 TCCFenceLog 已存在,说明 cancel 阶段已经提前触发并插入了记录,表示当前分支事务已经被标记为挂起(SUSPENDED)。此时,**try** 阶段插入将失败,抛出异常以阻止后续的业务代码执行,从而避免重复执行

总结

​ 整个流程看下来,发现TCC模式与AT模式是高度相似的,二者共享 TMTC,但使用了不同的 RM 实现

  • AT模式RMDataSourceManager,由 Seata 完全实现,通过自动构建 undo_log 来帮助资源回滚。
  • TCC模式RMTCCResourceManager,需要我们自己实现 confirmcancel 方法,来确认或回滚 try 阶段占用的资源,这也决定了它不能异步执行