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:reference、dubbo:reference),或者是带有@LocalTCC类注解的方法,都会由TccActionInterceptor作为切面来实现TCC在try阶段的代理
开启全局事务
首先TCC模式还是使用了@GlobalTransactional开启全局事务,这部分和AT模式是一样的,具体解析可以看之前的文章
try
调用带有@TwoPhaseBusinessAction注解的TccAction方法时,会进入TccActionInterceptor切面,该切面即是TCC的try阶段。核心的业务逻辑在ActionInterceptorHandler#proceed方法中实现,下面是重点流程的总结:
构建BusinessActionContext,并填充相关参数
使用TCC的RM(TCCResourceManager)向TC注册分支事务
- 注册时,
BusinessActionContext的所有信息会被封装在BranchRegisterRequest中,并存储在BranchTransactionDO里。之后在commit或rollback阶段时会传回这些数据 - TC处理TCC分支事务注册的逻辑和AT注册分支事务的逻辑是一套的,唯一的区别是TCC不需要获取锁资源(BranchSession#lock方法只对AT生效)
- 注册时,
根据
useTCCFence决定try阶段业务逻辑的执行方式useTCCFence=true:在事务的try阶段,会先插入一条状态为STATUS_TRIED的TCC_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 方法中,主要逻辑总结如下:
获取
TCCResource对象 据
@TwoPhaseBusinessAction注解中的name属性,获取对应的TCCResource对象,这个对象代表了当前需要提交的 TCC 资源构建
BusinessActionContext 使用
BranchTransactionDO填充BusinessActionContext,该上下文会在整个 TCC 流程中传递,确保业务信息一致性判断是否使用 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 数据(xid 和 branch_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 阶段未成功执行。在这种情况下,尝试插入一条状态为 SUSPENDED 的 tcc_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模式是高度相似的,二者共享 TM 和 TC,但使用了不同的 RM 实现
- AT模式 的 RM 是 DataSourceManager,由 Seata 完全实现,通过自动构建 undo_log 来帮助资源回滚。
- TCC模式 的 RM 是 TCCResourceManager,需要我们自己实现 confirm 和 cancel 方法,来确认或回滚 try 阶段占用的资源,这也决定了它不能异步执行