TCC 事务
分布式事务的典型乱象
凌晨 3 点,订单服务扣了库存,积分服务扣了积分,支付服务准备扣款——结果支付网关超时了。库存和积分已经扣了,但钱没扣。用户白得了积分,库存也少了。这就是分布式事务缺失时的典型乱象:部分成功,部分失败,系统状态陷入不一致。
2PC 和 3PC 试图通过协调者来解决这个问题,但协调者本身就是单点。TCC(Try-Confirm-Cancel)换了一个思路:把事务的「提交」和「回滚」逻辑全部交给业务方,让每个服务自己决定「我要预留什么资源」「成功了怎么确认」「失败了怎么撤销」。
这种「业务自包含」的设计,让 TCC 在灵活性和性能上,都比 2PC/3PC 往前走了一步。但它带来的挑战,也比想象中更复杂。
TCC 的核心思想
TCC 的本质
TCC 的本质是把一个分布式事务,拆成三个阶段:
- Try:预留资源,但不确定真正提交
- Confirm:所有服务 Try 成功后,正式确认执行
- Cancel:任何一个服务 Try 失败,执行撤销 :::
TCC 的关键假设是:每个业务操作都可以被拆解为「预留 → 确认/撤销」的形式。这要求业务设计者在设计接口时,就考虑到「可撤销」和「可确认」的语义。
- Try:预留资源,但不确定真正提交
- Confirm:所有服务 Try 成功后,正式确认执行
- Cancel:任何一个服务 Try 失败,执行撤销
TCC 的关键假设是:每个业务操作都可以被拆解为「预留 → 确认/撤销」的形式。这要求业务设计者在设计接口时,就考虑到「可撤销」和「可确认」的语义。
为什么叫 TCC?
TCC 这个名字来自三个阶段的首字母:
- Try:尝试预留资源,比如冻结库存、预扣余额
- Confirm:确认执行,比如真正扣减库存、真正扣款
- Cancel:取消执行,比如解冻库存、返还积分
Try 阶段更像是「占位」而不是「真正执行」。资源被预留了,但业务状态还没有真正变化。Confirm 才真正让状态落地,Cancel 则把预留的资源释放回去。
TCC 与 2PC 的关键区别
三阶段详解
Try 阶段:资源预留
Try 阶段的核心目标是「先占位,不执行」。每个参与者在这个阶段做两件事:
- 检查业务规则是否满足(比如库存是否足够)
- 预留资源(比如冻结库存、预占余额)
Try 阶段返回失败,意味着整个分布式事务应该回滚。所有已经 Try 成功的服务,需要执行 Cancel。
Confirm 阶段:确认执行
当所有参与者的 Try 都成功后,协调者(或业务编排方)发出 Confirm 指令。每个参与者把「预留」变为「真正执行」:
Confirm 阶段不应该失败。如果 Confirm 失败了(比如数据库宕机),这是一个严重故障,需要人工介入或触发告警。TCC 假设 Try 阶段已经做了充分的检查,Confirm 只是把预留变为执行,不应该有业务规则冲突。
Cancel 阶段:撤销执行
当任何一个参与者的 Try 失败,或者超时未收到 Confirm 时,所有已 Try 的参与者需要执行 Cancel:
Cancel 阶段必须成功。如果 Cancel 失败了,资源会一直处于「预留」状态,形成悬挂问题(后面会详细讲)。
流程图
TCC 的三大挑战
:::warning TCC 生产的三大经典问题
TCC 看起来简单,但在生产环境中,它面临三个经典问题:空回滚、幂等性、悬挂。不理解这三个问题,就无法正确实现 TCC。
1. 空回滚
问题描述:Try 阶段超时,事务管理器认为 Try 失败,发起 Cancel。但实际上这个服务根本没有执行过 Try,或者 Try 已经执行成功了只是响应超时。
如果 Cancel 逻辑没有判断「当前状态」,可能会把正常的库存又解冻一次,或者尝试解冻一个不存在的冻结记录。这就是「空回滚」。
解决方案:Cancel 操作必须先查询当前状态,再决定如何处理:
2. 幂等性
问题描述:Try/Confirm/Cancel 任何一阶段都可能因为网络问题超时,事务管理器会重试。服务必须保证「同一笔事务的同一阶段,多次执行效果等同于执行一次」。
如果 Confirm 逻辑没有幂等处理,10 件库存会被扣减两次。这就是「幂等性问题」。
解决方案:为每笔分布式事务生成全局唯一的事务 ID(XID),并在每个操作中记录「已执行」状态:
幂等性是 TCC 实现中最容易被忽视的环节。建议使用「事务日志表」来记录每个阶段是否已执行,并在执行业务操作之前先检查日志。
3. 悬挂
问题描述:Cancel 比 Try 先执行了,或者 Try 未执行但收到了 Confirm/Cancel。这通常发生在网络乱序或服务重启时。
更严重的情况是:Try 因为网络问题一直没执行成功,但 Cancel 执行了,导致资源被错误释放。然后 Try 最终成功了,但此时资源已经被释放了,造成业务状态混乱。
解决方案:使用「防悬挂检查」——在 Try 执行前,检查是否有对应的 Cancel 记录:
Java 代码示例:完整的 TCC 事务服务
下面是一个完整版的 TCC 库存服务实现,涵盖了 Try/Confirm/Cancel 三个阶段以及幂等性处理:
Seata TCC 模式
Seata 是阿里开源的分布式事务解决方案,提供了 AT、TCC、Saga 三种模式。其中 TCC 模式与上述实现思路一致,但提供了更多基础设施支持。
Seata TCC 的核心组件
- TC(Transaction Coordinator):独立部署的协调者服务,负责管理全局事务状态
- TM(Transaction Manager):事务管理器,发起全局事务
- RM(Resource Manager):资源管理器,每个微服务
Seata TCC 注解使用
- 事务日志自动管理:Seata 会自动记录 Try/Confirm/Cancel 的状态,无需自己维护事务日志表
- 隔离性保证:通过 branch_session 表保证分布式事务的隔离性
- 高性能:Try 阶段只是预留,不涉及数据库行锁
- 生态完善:与 Spring Cloud、Dubbo 等主流框架无缝集成 :::
权衡矩阵
TCC vs 2PC vs 3PC 对比
常见错误与反模式
:::danger TCC 实现的三大致命错误
在实际生产中,TCC 实现有三个最容易犯的错误,必须避免:
错误 1:Try 阶段做真正的业务操作
正确做法:Try 阶段只做「冻结」或「预留」,不改变业务的最终状态。Confirm 才真正执行,Cancel 只需要「解冻」或「返还」。
错误 2:不处理幂等性
正确做法:每次操作前检查事务日志,确保同一事务的同一阶段不会被重复执行。
错误 3:Confirm 失败时不记录
正确做法:Confirm 阶段不应该失败(或者只记录失败日志,不抛异常)。如果真的失败,需要人工介入或触发告警。
术语表
延伸思考
TCC 的适用场景与局限
TCC 给我们最重要的启示是:业务层面的事务控制比数据库层面的事务控制更灵活。通过把「提交」和「回滚」的权力交给业务方,TCC 打破了 2PC 的性能瓶颈,但代价是开发成本的增加。
在实际项目中,TCC 适合以下场景:
- 库存扣减、余额预占、优惠券锁定等「可逆」的业务操作
- 对性能要求高、无法忍受长事务锁的场景
- 业务逻辑可以清晰拆分为「预留 → 确认/撤销」的场景
如果你的业务很难拆分为「可撤销」的形式(比如发送短信、发送邮件等副作用操作),TCC 可能不是最佳选择。这时可以考虑 Saga 模式,它用「正向补偿」代替「回滚」,更适合长链路业务场景。