分布式事务:TCC 补偿式事务
一、什么是 TCC 事务
TCC 模式可以解决 2PC 中的资源锁定和阻塞问题,减少资源锁定时间。
TCC 是 try 、confirm、cancel 三个词语的缩写,TCC 要求每个分支事务实现三个操作:预处理Try 、确认Confirm、撤销 Cancel 。Try 操作坐业务检查及资源预留,Confirm 做业务确认操作,Cancel 实现一个 与 Try 相反的操作即回滚操作。TM 首先发起所有的分支事务的 try 操作,任何一个分支事务的 try 操作执行失败,TM 将会发起所有分支事务的cancel 操作,若try操作全部成功,TM 将会发起所有分支事务的Confirm 操作,其中 confirm/cancel 操作若执行失败,TM 会进行重试。
TCC 分为三个阶段:
-
Try 阶段做业务检查(一致性)及资源预留(隔离),此阶段仅是一个初步操作,它和后续的 confirm 一起才能真正构成一个完整的业务逻辑。
-
Confirm 阶段是做确认提交,Try阶段所有分支事务执行成功后开始执行 Confirm。通常情况下,采用TCC 则认为Confirm 阶段是不会出错的。即:只要 Try成功,Confirm一定成功,若Confirm 阶段真的出错了,就需要引入重试机制或人工处理。
-
Cancel 阶段是在业务执行错误需要回滚的状态下执行分支事务的业务取消,预留资源释放。通常情况下,采用TCC 则认为Cancel 阶段也是一定成功的。若Cancel 阶段真的出错了,需要引入重试机制会人工处理。
-
TM事务管理器:TM事务管理器可以实现为独立的服务,也可以让全局事务发起方充当TM 的角色,TM 独立出来是为了称为公用组件,是为了考虑系统结构和软件复用。
TM在发起全局事务时生成全局事务记录,全局事务 ID 贯穿整个分布式事务调用链条,用来记录事务上下文,追踪和记录状态,由于Confirm 和 cancel 失败需进行重试,因此需要实现为幂等,幂等性是指同一个操作无论请求多少次,其结果都相同。
执行流程:
- 1、启动事务
- 2、调用各个服务中的 try 接口,分布尝试执行事务
- 3、判断 try 的结果,如果都成功了就提交事务,否则就要回滚事务
- 4、提交事务就调用Confirm 接口,回滚事务就调用Cancel 接口
- 5、系统判断要不要回滚是通过 ”是否抛出异常“ 来判断的,而不是根据返回值。
二、TCC 解决方案
2.1 概述
目前主流的 TCC 方案如下:
- Seata :阿里云推出的组件,支持较多方案,主推AT(二阶段+分布式锁)
- tcc-transaction:不和底层rpc耦合,使用dubbo,http,thrift,webservice都可
- tx-lcn:支持常用的dubbo,springcloud框架,维护不频繁,热度有所下降
- hmily:国内工程师开发,异步高性能TCC框架,适应国内环境
- ByteTcc:国内开发,兼容JTA规范的TCC框架
- EasyTransaction:柔性事务、TCC、SAGA、可靠消息等功能齐全,一站式解决
相对来说,Hmily 它更加轻量级,无需部署独立的 TCC 协调器,也适合Dubbo 和 SpringCloud 环境,它具备以下优点:
- 无缝集成Spring,Spring boot start。
- 无缝集成Dubbo,SpringCloud,Motan等rpc框架。
- 多种事务日志的存储方式(redis,mongdb,mysql等)。
- 多种不同日志序列化方式(Kryo,protostuff,hession)。
- 事务自动恢复。
- 支持内嵌事务的依赖传递。
- 代码零侵入,配置简单灵活。
2.2 TCC中的特别处理
空回滚
在没有调用TCC中Try方法的情况下,调用了第二阶段的Cancel方法,Cancel方法需要直接识别出来这是一次空回滚,直接返回成功结果。
出现原因:分支事务所在系统服务宕机或者网络波段,分支事务的调用记录是失败的,该情况下其实分支事务没有进行Try操作,当故障恢复后,分布式事务进行了回滚则会调用二阶段的Cancel方法,既而形成了空回滚。
解决方法:已知全局事务id会贯穿整个全局分布式事务的调用链,额外增加一张分支事务记录表,其中有全局事务id和分支事务id,每一次成功的Try执行后插入一条分支事务执行记录,第二阶段Cancel执行时读取该表记录,如果该分支事务对应的执行记录存在,就回滚,如果不存在就认为是空回滚,直接返回成功。
幂等性 为了保证第二阶段中出现的失败情况,Hmily会有重试机制,此时就会出现幂等性问题。如果Cancel执行过程中没有保证好幂等性问题,会导致数据污染。
悬挂 悬挂就是Cancel先于Try执行了。
出现原因:当全局事务发起者通过RPC方式调用分支分支事务执行Try的时候,出现了调用网络延迟的问题,此时TM会认为RPC调用超时需要回滚,但是可能这次RPC的Try请求在回滚之后执行成功了。此次Try操作预留的资源只有该分布式事务可以使用,该次预留的资源无法进行后续的处理,这就是选个挂。
解决思路:在执行第一阶段Try操作之前,要在事务记录表中是否有该分支事务对应的二阶段事务,如果有记录就不执行Try。
总结:
- 在TCC模式下,所有操作要保证幂等性
- 需要有事务的执行记录
- 执行Try和Cancel时候都需要进行操作记录判断
2.3 优势和缺点
-
优势:TCC执行的每一阶段都会提交本地事务并释放锁,并不需要等待其他事务的执行结果。而如果其他事务执行失败,最后不是回滚,而是执行补偿操作。这样就避免了资源的长期锁定和阻塞等待,执行效率比较高,属于性能比较好的分布式事务方式。
-
缺点:
- 代码侵入:需要认为编写代码实现 try 、confirm、 cancel 代码侵入较多
- 开发成本高:一个业务需要拆分成 3个步骤,分别编写业务实现,业务编写比较复杂
- 安全性考虑:cancel 动作如果执行失败,资源就无法释放,需要引入重试机制,而重试导致重复执行哦,还有考虑重试的幂等性问题。
2.4 案例
场景:A 转账给30元 给 B,A 和 B 在不同的服务。
2.4.1 方案1:
账户 A
try:
检查余额是否够30元
扣减30元
confirm:
空
cancel:
增加30元
账户B
try:
增加30元
confirm:
空
cancel:
减少30元
该方案存在问题:
- 1、没有做幂等控制:try、confirm 和 cancel 没有幂等控制,都可能重复执行
- 2、空回滚:如果A 的try没有执行,在cancel 的时候就多加了30
- 3、账户B 的try 阶段增加了30元,可能在try执行完成后被其他线程消费了,然后导致无法减少30元而报错。
2.4.2 优化方案
账户A
try:
try幂等校验
try悬挂处理
检查余额是否够30元
扣减30元
confirm:
空
cancel:
cancel幂等校验
cancel空回滚处理
增加余额30元
账户B
try:
空
confirm:
confirm幂等校验
正式增加30元
cancel:
空
2.5 使用场景
- 对事务有一定的一致性要求(最终一致性)
- 对性能要求较高
- 开发人员具备较高的编码能力和幂等处理经验
[转:分布式事务:TCC]