分布式事务: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]