Seata TCC 模式实现
问题背景
2024年3月,我们团队在一个库存扣减场景中引入了 Seata TCC 模式。业务逻辑是:用户下单时,预扣库存 -> 创建订单 -> 扣减账户余额。设计初衷是 TCC 模式没有全局锁,性能应该比 AT 模式好。
灰度上线第三天,运营同学发现了一个诡异的问题:有一批订单的库存被扣了,但订单状态是"已取消"。更诡异的是,这些订单的取消时间是早于下单时间的。
排查了 4 个小时,终于定位到根因——空回滚。
问题的时序是这样的:
- Try 阶段超时或失败,TC 认为分支事务失败,向参与者发送 Cancel 指令
- 但实际上 Try 的网络包已经到达了库存服务,只是响应超时了
- 库存服务收到 Cancel 时,实际的库存扣减已经发生了(Try 执行成功,只是回包丢了)
- Cancel 执行了"归还库存"操作,导致库存被多还了 1 个
这次事故让我们意识到:TCC 模式虽然看起来简单(Try/Confirm/Cancel 三个接口),但空回滚和防悬挂的处理比想象中复杂得多。
【架构权衡】 Seata TCC 与独立 TCC(如 Hmily、ByteTCC)的核心区别在于TC 的统一协调能力。Seata TC 提供了全局事务管理、状态持久化、可视化监控等基础设施,让 TCC 的落地门槛大幅降低。但这也意味着你需要理解 Seata TC 的工作原理,否则会踩到空回滚、悬挂等经典 TCC 陷阱。
问题定义
TCC(Try-Confirm-Cancel)是一种补偿式的分布式事务方案。其核心思想是:将一个分布式事务拆分为三个阶段:
- Try:预留资源(锁定资源、预扣库存等),所有参与者的 Try 都成功,整个事务才能进入 Confirm 阶段
- Confirm:确认执行,使用 Try 阶段预留的资源(真正扣减库存、真正转账等)
- Cancel:取消执行,释放 Try 阶段预留的资源(归还库存、撤销冻结金额等)
TCC 与 AT 模式的关键区别在于:TCC 不依赖数据库锁,资源的锁定和释放完全由业务代码控制。
核心设计
TCC 三个接口的设计原则
TCC 的三个接口各有明确的职责和幂等性要求:
Try 接口的职责:
Confirm 接口的职责:
Cancel 接口的职责:
TCC 模式最常见的三个陷阱:
- 空回滚:Try 没执行但收到了 Cancel。解决方案:使用 TCC 日志或数据库状态来检测。
- 幂等性:Try/Confirm/Cancel 都会被 TC 多次调用。解决方案:每个方法都要有幂等检查。
- 悬挂:Cancel 先执行了,Try 后执行。解决方案:Try 执行前检查是否已经 Cancel 过。 :::
Seata TCC 的注册流程
Seata TCC 与独立 TCC 框架(如 Hmily)的一个核心区别是:Seata TC 负责全局事务的协调和状态管理。
超时控制
Seata TC 维护了全局事务的超时时间(默认 60 秒):
当全局事务超时(默认 60 秒)时,TC 会自动触发回滚。但如果 Try 阶段本身执行较慢(比如外部支付网关调用),可能会在 Cancel 执行时,Try 刚刚完成——导致Cancel 和 Try 的结果同时到达,引发数据不一致。
:::tip 💡 对于涉及外部服务的 Try 操作,建议将全局事务超时时间设置得足够长(如 120 秒),同时在 Try 方法内部设置合理的超时(如 HTTP 调用设置 10 秒超时)。这样可以避免"Try 还在执行但全局超时已经到了"的情况。
空回滚与防悬挂的完整实现
空回滚和防悬挂是 TCC 模式的两个经典问题。Seata 提供了 @TwoPhaseBusinessAction 注解来简化处理,但业务代码仍需配合:
AT 模式与 TCC 模式对比
【架构权衡】 Seata TCC 的优势在于没有全局锁,适合高并发场景。但代价是代码侵入性高、开发和维护成本大。在实际项目中,我建议:核心交易链路用 TCC(库存、余额等高频资源),辅助流程用 AT 或 Saga(查询、统计等)。
生产避坑
坑一:Try 阶段的资源预留要"假扣"
TCC 的 Try 阶段不能直接扣减资源,而是"预留"或"冻结"。
库存表的设计需要增加 frozen 字段:
坑二:Confirm 和 Cancel 的超时处理
如果 TC 向分支发送了 Confirm 指令但分支没响应(网络分区),TC 会重试 Confirm。这要求 Confirm 必须是幂等的。
但更危险的是悬挂问题:如果 Cancel 先执行了,然后 Try 才执行(因为 Try 之前被网络延迟了),会导致资源被错误预留。
解决方案:在 Try 方法中检查 TCC 日志状态。如果已经 Cancel 过,直接拒绝 Try 并返回 false。
坑三:TCC 日志不能省
很多团队为了简化实现,省略了 TCC 日志(用于记录 Try/Confirm/Cancel 状态的表)。在没有日志的情况下,空回滚和幂等性都无法保证。
工程代价
落地 Checklist
- 设计支持"冻结/解冻"状态的业务表结构(库存表增加
frozen字段等) - 实现 TCC 三个接口,确保幂等性、防空回滚、防悬挂
- 创建 TCC 日志表(或复用 Seata 的 undolog 表)
- 配置全局事务超时时间(建议 120 秒)
- 在 Try 阶段做悬挂检查(查询 TCC 日志)
- 在 Cancel 阶段做空回滚处理(TCC 日志不存在则直接返回)
- Confirm 和 Cancel 必须是幂等的(可重复调用不产生副作用)
- 单元测试覆盖:正常流程、空回滚流程、幂等重试流程、悬挂流程
- 压测验证 Try/Confirm/Cancel 的 RT 和吞吐量
- 在 Seata 控制台配置全局事务监控告警