今天我们来深入探讨一下阿里巴巴 Seata 框架在 1.5.1 版本中,是如何通过创新的机制解决 TCC 模式长期存在的三大顽疾:幂等、悬挂和空回滚的。
1 TCC 模式回顾
TCC 是最经典的分布式事务解决方案之一,它将一个分布式事务拆分为两个阶段执行:
- Try 阶段:尝试执行业务,完成所有资源的检查和预留。
- Confirm/Cancel 阶段:根据 Try 阶段的结果,进行最终的提交(Confirm)或回滚(Cancel)。
以一个电商下单场景为例,涉及订单、库存、账户三个服务。在 Try 阶段,我们并非直接扣减库存和金额,而是将它们“预留”或“冻结”在一个中间状态。如果所有服务的 Try 操作都成功,则进入 Confirm 阶段,将预留的资源正式扣除。如果任何一个服务的 Try 操作失败,则进入 Cancel 阶段,释放所有预留的资源。
关键点:Try 阶段的操作必须是提交本地事务的。例如,冻结账户金额,必须真实地从客户账户中将这笔钱扣除并转入一个中间态。如果不这样做,在 Confirm 阶段可能因为账户余额不足而导致问题。
1.1 Try-Confirm 流程
Try 阶段预留资源,Confirm 阶段扣除资源。流程如下图所示:

1.2 Try-Cancel 流程
Try 阶段预留资源时,若某个服务(如扣减库存)失败,则全局事务回滚,在 Cancel 阶段释放所有已预留的资源。流程如下图所示:

2 TCC 模式的优势
TCC 模式最显著的优点是高性能。因为它在 Try 阶段就真实提交了本地事务,将资源锁定在业务层面(中间态),而非数据库层面,避免了长时间的资源阻塞等待。
此外,TCC 模式还可以进行深度优化:
2.1 异步提交
Try 阶段成功后,可以不立即执行 Confirm/Cancel,而是认为全局事务已结束,通过后台定时任务异步地完成最终资源扣除或释放,这能极大提升系统的吞吐量。
2.2 同库模式优化
在标准 TCC 模型中,涉及三个角色:
- TM (Transaction Manager):管理全局事务的开启、提交或回滚。
- RM (Resource Manager):管理分支事务,负责执行实际的 Try、Confirm、Cancel 操作。
- TC (Transaction Coordinator):协调全局事务和分支事务的状态。
标准流程中,RM 与 TC 之间需要进行多次 RPC 调用来注册和报告状态。Seata 的“同库模式”对此进行了优化,将分支事务状态记录在本地,从而减少了约 50% 的 RPC 调用,性能得到显著提升。


3 RM 端代码示例
以一个库存服务为例,其 TCC 接口的 Java 代码通常如下所示:
@LocalTCC
public interface StorageService {
/**
* 扣减库存 (Try阶段)
* @param xid 全局xid
* @param productId 产品id
* @param count 数量
* @return
*/
@TwoPhaseBusinessAction(name = "storageApi", commitMethod = "commit", rollbackMethod = "rollback", useTCCFence = true)
boolean decrease(String xid, Long productId, Integer count);
/**
* 提交事务 (Confirm阶段)
* @param actionContext
* @return
*/
boolean commit(BusinessActionContext actionContext);
/**
* 回滚事务 (Cancel阶段)
* @param actionContext
* @return
*/
boolean rollback(BusinessActionContext actionContext);
}
通过 @LocalTCC 注解,RM 在初始化时会向 TC 注册这个分支事务。在 Try 方法(decrease)上的 @TwoPhaseBusinessAction 注解定义了该分支事务的 resourceId、提交方法和回滚方法。其中 useTCCFence = true 属性是 Seata 1.5.1 解决三大问题的关键,我们下一节详细讲解。
4 Seata 1.5.1 的解决方案
在 Seata 1.5.1 版本之前,幂等、悬挂和空回滚需要开发者自行在业务代码中处理,逻辑复杂且容易出错。新版本引入了一张核心表 tcc_fence_log,通过记录事务状态来系统性地解决这些问题。上文提到的 useTCCFence 属性就是用来启用这套防悬挂机制的开关,默认是 false。
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;
4.1 解决幂等问题
问题场景:在 Confirm/Cancel 阶段,网络波动可能导致 TC 未收到 RM 的响应,从而触发重试。如果 RM 的 Confirm/Cancel 操作不是幂等的,重试就会导致资源被重复扣减或释放。
Seata 解决方案:
在 Confirm 阶段,TCCFenceHandler.commitFence 方法会先查询 tcc_fence_log 表,检查当前分支事务的状态。
- 如果状态已经是
STATUS_COMMITTED(已提交),则直接返回成功,不再执行业务提交逻辑。
- 如果状态是
STATUS_TRIED(已尝试),则将其更新为 STATUS_COMMITTED,并执行真正的业务提交方法。
这样,无论 TC 重试多少次,RM 端都只会成功提交一次。Cancel 阶段的幂等逻辑与此类似。
核心判断逻辑代码简化示意:
public static boolean commitFence(...) {
// ... 获取数据库连接
TCCFenceDO tccFenceDO = TCC_FENCE_DAO.queryTCCFenceDO(conn, xid, branchId);
if (tccFenceDO == null) {
// 记录不存在,抛出异常
}
// 关键幂等判断
if (TCCFenceConstant.STATUS_COMMITTED == tccFenceDO.getStatus()) {
LOGGER.info(“Branch transaction has already committed before. idempotency rejected...”);
return true; // 已提交,直接返回成功
}
// ... 其他状态判断
// 更新状态并执行业务方法
return updateStatusAndInvokeTargetMethod(..., TCCFenceConstant.STATUS_COMMITTED, ...);
}
4.2 解决空回滚问题
问题场景:在 分布式事务 中,某个 RM 分支的 Try 操作可能因为网络超时或节点宕机根本没有执行,但全局事务已决定回滚。此时,TC 会向该 RM 发起 Cancel 指令。如果 RM 直接执行 Cancel 逻辑,就会“空回滚”一个从未预留过的资源。

Seata 解决方案:
- Try 阶段:执行成功时,会向
tcc_fence_log 表插入一条状态为 STATUS_TRIED 的记录。
- Cancel 阶段:执行
rollbackFence 方法时,先查询 tcc_fence_log 表。
- 如果没有找到对应记录,说明 Try 阶段根本未执行,此时直接返回成功,并不执行业务回滚逻辑,从而避免了空回滚。
- 如果找到了记录且状态为
STATUS_TRIED,则正常执行业务回滚。
核心逻辑代码简化:
public static boolean rollbackFence(...) {
// ... 获取数据库连接
TCCFenceDO tccFenceDO = TCC_FENCE_DAO.queryTCCFenceDO(conn, xid, branchId);
// 空回滚处理
if (tccFenceDO == null) {
// 记录不存在,说明try没执行,不进行回滚操作
return true;
} else {
// ... 其他状态判断(如已回滚则幂等返回)
}
// 正常更新状态并执行业务回滚
return updateStatusAndInvokeTargetMethod(..., TCCFenceConstant.STATUS_ROLLBACKED, ...);
}
4.3 解决悬挂问题
问题场景:这是最棘手的问题。由于网络阻塞,Cancel 指令可能先于 Try 指令到达 RM。RM 执行了 Cancel(由于空回滚逻辑,可能什么都没做),之后迟到的 Try 指令才到达并成功执行,预留了资源。此时全局事务已结束,这些预留的资源将永远无法被释放。

Seata 解决方案:
结合空回滚和幂等的逻辑,形成了一个闭环防御:
- Cancel 先到:执行
rollbackFence 方法,查询 tcc_fence_log 表,发现没有记录(因为 Try 未执行)。此时,它不仅不执行业务回滚,还会插入一条状态为 STATUS_SUSPENDED(悬挂) 的记录。
- Try 后到:执行
prepareFence 方法,尝试插入状态为 STATUS_TRIED 的记录。由于 (xid, branch_id) 是主键,而 STATUS_SUSPENDED 记录已存在,会导致主键冲突插入失败。Seata 捕获到这个异常后,会识别出这是悬挂场景,从而阻止 Try 业务执行,并清理日志。
这样,无论指令顺序如何,都保证了“Try 成功执行”和“全局事务回滚”这两个事件是互斥的,从而防止了资源悬挂。
相关代码逻辑:
// Cancel阶段先到,插入防悬挂记录
if (tccFenceDO == null) {
boolean result = insertTCCFenceLog(conn, xid, branchId, actionName, TCCFenceConstant.STATUS_SUSPENDED);
return true; // 插入悬挂记录后直接返回,不执行业务回滚
}
// Try阶段后到,插入记录时发生主键冲突
catch (TCCFenceException e) {
if (e.getErrcode() == FrameworkErrorCode.DuplicateKeyException) {
LOGGER.error(“Branch transaction has already rollbacked before,prepare fence failed...”);
addToLogCleanQueue(xid, branchId); // 识别为悬挂,加入清理队列
}
// ... 抛出异常,Try业务不会被执行
}
重要细节:queryTCCFenceDO 方法在查询 SQL 中使用了 SELECT ... FOR UPDATE。这确保了在 Cancel 阶段查询并可能插入 STATUS_SUSPENDED 记录的这个过程是原子的,防止在并发场景下,Try 和 Cancel 同时判断无记录而都去插入,从而破坏防悬挂逻辑。
5 总结与思考
TCC 模式因其高性能特点,在要求严格的金融、电商等场景中应用广泛,但幂等、悬挂和空回滚一直是其落地过程中必须谨慎处理的“暗礁”。Seata 框架在 1.5.1 版本中,通过引入 tcc_fence_log 事务控制表,以一种优雅且系统性的方式完美解决了这三大经典难题,将复杂的分布式事务问题收敛到了框架内部,极大地减轻了开发者的心智负担。
此外,Seata 框架通过代理数据源,确保了对 tcc_fence_log 表的操作与 RM 的业务数据库操作处于同一个本地事务中,实现了“同生共死”,保证了事务日志与业务状态的一致性。
对于正在使用或考虑使用 TCC 模式解决 分布式事务 的团队来说,深入理解 Seata 的这套机制,不仅能帮助我们更好地使用这个强大的工具,也能深化我们对分布式事务本质的认识。如果你想了解更多关于 Java 分布式系统的实战经验与深度解析,欢迎访问 云栈社区 与更多开发者交流探讨。