本文目录导读:

这是一个非常经典的分布式事务问题,TCC(Try-Confirm-Cancel)是一种补偿型事务模式,通过业务逻辑的拆解来保证分布式系统的最终一致性。
下面我从核心思想、执行流程、补偿机制、设计难点、与2PC的区别、最佳实践这六个方面为你详细解析。
核心思想:业务拆分为三个阶段
TCC 的核心思想是:将一个完整的业务操作,拆分成三个阶段,并为每个阶段预留一个“反操作”或“修复操作”,它不再依赖于数据库的本地事务,而是由业务代码来控制。
- Try(尝试预留): 检查并预留业务资源,确保后续操作有足够的条件执行。这个阶段通常不会真正提交事务,而是锁定资源。
- Confirm(确认提交): 如果所有参与者的 Try 阶段都成功,则进入 Confirm 阶段,这个阶段真正执行业务操作,释放 Try 阶段预留的资源。这个阶段的逻辑通常是幂等的。
- Cancel(取消回滚): 如果任何一个参与者的 Try 阶段失败(超时、报错等),则对所有已成功的 Try 操作执行 Cancel 操作,释放预留的资源,回滚到初始状态。
执行流程与补偿机制(核心)
以经典的“跨行转账”为例:A银行扣100元,B银行加100元。
1 正常流程(一路绿灯)
A银行 (扣款) 协调器(TC) B银行 (加款) | | | |---Try(冻结100元)-->| | |<-Try OK-----------| | | |---Try(请求加款)---->| | |<--Try OK------------| | | | | | (所有Try都成功) | | | | |<-Confirm(扣款100)-| | |<-Confirm OK-------| | | |--Confirm(加款100)--> | |<--Confirm OK--------| | | |
- Try阶段:
- A:检查账户余额,冻结100元(状态变为“冻结中”,余额不减,可用余额减少)。
- B:检查账户是否存在,预留加款额度(不是直接加100,而是记录一个“待加款”状态)。
- Confirm阶段:
- A:将冻结的100元真正扣除,解除冻结。
- B:将预留的100元真正加到余额中,消除“待加款”状态。
- Cancel阶段: (在正常流程中不执行)
2 异常流程与补偿机制(谁出事,谁回滚)
场景:A成功冻结,B因账户异常Try失败。
A银行 (扣款) 协调器(TC) B银行 (加款) | | | |---Try(冻结100元)-->| | |<-Try OK-----------| | | |---Try(请求加款)---->| | |<--Try_FAILED--------| // B报错 | | | | | (检测到失败) | | | | |<--Cancel(解冻100)--| | // **补偿:A必须回滚** |<-Cancel OK---------| | | | |
- 检测到失败: 协调器发现B的Try失败了。
- 触发补偿: 协调器立即调用A的 Cancel 服务。
- Cancel逻辑(补偿):
- A:找到之前冻结的100元记录,将其解冻(恢复可用余额)。
- 最终结果:A账户资金没动,B账户没变化,系统回滚到初始状态,保证了最终一致性。
TCC的设计难点与“陷阱”
TCC看起来很完美,但在实际开发中,这3个方面很容易踩坑:
-
空回滚(Empty Rollback):
- 问题: 如果一个微服务的Try方法因网络问题从未被执行(协调器没收到请求),但协调器却直接调用了它的Cancel方法,此时该服务没有需要回滚的资源,Cancel必须能正确处理空状态。
- 解决方案: Cancel方法需要判断是否存在Try的记录,如果不存在,则直接返回成功(幂等处理)。
-
幂等(Idempotency):
- 问题: 网络超时、协调器重试等都可能导致Try/Confirm/Cancel被调用多次。
- 解决方案: 每个请求都必须有全局唯一的
事务ID,在资源表中记录事务ID,通过唯一索引或状态机来保证同一个事务ID的操作只生效一次。
-
悬挂(Suspension):
- 问题: Try请求因为网络延迟,在Cancel请求之后才到达,这时Cancel已经完成,Try才执行,导致“冻结”了无法被解冻的资源(永久悬挂)。
- 解决方案: Cancel方法执行后,记录一个“已取消”状态,当Try请求到达时,检查该状态,如果已取消,则拒绝执行Try并直接返回成功(或抛出异常)。
TCC vs. 2PC(两阶段提交)
很多人容易混淆两者,核心区别在于资源锁定粒度和事务控制权:
| 特性 | 2PC(两阶段提交) | TCC(补偿型) |
|---|---|---|
| 资源锁定 | 数据库/系统级别(如数据库行锁、全局锁),冲突较高。 | 业务逻辑级别(通过状态字段、预留表),冲突较低。 |
| 事务控制 | 由数据库或中间件控制,属于强一致性(XA协议)。 | 由业务代码控制,属于最终一致性(BASE理论)。 |
| 性能 | 较低,因为要锁定资源直到事务结束,高并发下易死锁。 | 较高,因为只锁定业务资源,不阻塞数据库。 |
| 故障处理 | 阻塞与超时:协调器宕机,参与者会一直阻塞等着,复杂性极高。 | 补偿与重试:利用消息队列、重试机制处理,抗风险能力强。 |
| 适用场景 | 短事务、低并发、对一致性要求极高的场景(如金融核心结算)。 | 长事务、高并发、对性能要求高的场景(如电商下单、积分、支付)。 |
最佳实践建议
-
引入成熟框架: 不要手写TCC,容易出错,推荐使用:
- Seata TCC模式(阿里):主流方案,与Spring Cloud/ Alibaba集成好。
- ByteTCC(开源):轻量级。
- TCC-Transaction(开源):经典方案。
-
业务设计原则: 尽量把Try阶段的资源预留设计得轻量级(比如只改状态,不锁大量数据行),而复杂逻辑尽量放在Confirm阶段。
-
日志与监控: 所有Try/Confirm/Cancel的调用都必须有完整日志,并记录事务状态,搭建报警系统,监控长时间处于“pending(待处理)”状态的事务。
-
重试与补偿策略: 为Confirm和Cancel设置重试机制(如指数退避、基于消息队列的重试),直到业务逻辑成功或达到最大重试次数后触发人工介入。
-
业务隔离性: 在Try阶段预留的资源,对其他业务是可见但不可修改的(冻结”状态),这要求在数据库查询余额时,要过滤掉冻结部分。
TCC通过业务逻辑的分解与补偿,用最终一致性换取了高性能和高可用,但它需要你手动实现空回滚、幂等、悬挂这三个关键细节,且对业务侵入性较强,如果你的系统并发量不高且对一致性要求极高(比如1ms都不能差),2PC/XA协议可能更合适;但在互联网高并发场景下,TCC通常是更优解。