天天看点

分布式事务解决方案之TCC

目录

一、什么是TCC

二、TM 事务管理器

三、TCC解决方案

四、TCC需要注意的问题

一、什么是TCC

TCC是Try、Confirm、Cancel三个词语的缩写,TCC要求每个分支事务实现三个操作:预处理Try、确认Confirm、撤销Cancel。

Try 操作做业务检查及资源预留 , Confirm做业务确认操作 , Cancel实现一个与Try相反的操作即回滚操作 。

TM首先发起所有的分支事务的try操作,任何一个分支事务的try操作执行失败,TM将会发起所有分支事务的Cancel操作,若try操作全部成功,TM将会发起所有分支事务的Confifirm操作,其中Confirm/Cancel操作若执行失败,TM会进行重试。

成功情况:

分布式事务解决方案之TCC

失败情况:

分布式事务解决方案之TCC

TCC分为三个阶段

  • Try 阶段是做业务检查(一致性)及资源预留(隔离) ,此阶段仅是一个初步操作,它和后续的Confirm 一起才能真正构成一个完整的业务逻辑。
  • Confirm 阶段是做 确认提交 ,Try阶段所有分支事务执行成功后开始执行 Confirm。通常情况下,采用TCC则认为 Confirm阶段是不会出错的。即: 只要Try成功,Confirm一定成功 。若Confirm阶段真的出错了,需引入重试机制或人工处理。
  • Cancel 阶段是在业务执行错误 需要回滚的状态下执行分支事务的业务取消 ,预留 资源释放。通常情况下,采用TCC则 认为Cancel阶段也是一定成功 的。若Cancel阶段真的出错了,需引入重试机制或人工处理。

二、TM 事务管理器

TM事务管理器可以实现为独立的服务,也可以让全局事务发起方充当 TM 的角色,TM 独立出来是为了成为公 用组件,是为了考虑系统结构和软件复用。

TM 在发起全局事务时生成全局事务记录,全局事务 ID 贯穿整个分布式事务调用链条,用来记录事务上下文, 追踪和记录状态,由于 Confirm 和 Cancel 失败需进行重试,因此需要实现为幂等,幂等性是指同一个操作无论请求多少次,其结果都相同。

三、TCC解决方案

框架名称 GitHub地址
tcc-transaction https://github.com/changmingxie/tcc-transaction
Hmily https://github.com/yu199195/hmily
ByteTCC https://github.com/liuyangming/ByteTCC
EasyTransaction https://github.com/QNJR-GROUP/EasyTransaction

四、TCC需要注意的问题

1、空回滚

在没有调用 TCC 资源 Try 方法的情况下,调用了二阶段的 Cancel 方法,Cancel 方法需要 识别出这是一个空回滚 ,然后直接返回成功。

出现原因:是当一个分支事务所在服务宕机或网络异常,分支事务调用记录为失败,这个时候其实是没有执行Try阶段,当故障恢复后,分布式事务进行回滚则会调用二阶段的Cancel方法,从而形成空回滚。

解决方法:识别出这个空回滚。需要知道一阶段是否执行,如果执行了,那就是正常回滚;如果没执行,那就是空回滚。前面已经说过TM在发起全局事务时生成全局事务记录,全局事务ID贯穿整个分布式事务调用链条。再额外增加一张分支事务记录表,其中有全局事务 ID 和分支事务 ID,第一阶段 Try 方法里会插入一条记录,表示一阶段执行了。

//在cancel中cancel空回滚处理,如果try没有执行,cancel不允许执行
if(accountInfoDao.isExistTry(transId)<=0){
    log.info("bank1 空回滚处理,try没有执行,不允许cancel执行,xid:{}",transId);
    return ;
}
           

2、幂等

为了保证TCC二阶段提交重试机制不会引发数据不一致,要求 TCC 的二阶段 Try、Confirm 和 Cancel 接口保证幂等,这样不会重复使用或者释放资源。如果幂等控制没有做好,很有可能导致数据不一致等严重问题。

3、悬挂

悬挂就是对于一个分布式事务,其二阶段 Cancel 接口比 Try 接口先执行。

出现原因:RPC 调用分支事务try时,先注册分支事务,再执行RPC调用,如果此时 RPC 调用的网络发生拥堵,通常 RPC 调用是有超时时间的, RPC 超时 以后,TM就会通知RM 回滚 该分布式事务,可能回滚完,RPC 请求才到达参与者真正执行,而一个 Try 方法预留的业务资源。

解决思路:如果二阶段执行完成,那一阶段就不能再继续执行。在执行一阶段事务时判断在该全局事务下, “分支事务记录”表中是否已经有二阶段事务记录 ,如果有则不执行Try。

举例,场景为 A 转账 30 元给 B,A 和 B 账户在不同的服务。

方案:

账户 A

try:
    检查余额是否够30元
    扣减30元
 
confirm:
    空
 
cancel:
    增加30元
           

账户 B

try:
    增加30元
 
confirm:
    空
 
cancel:
    减少30元
           

方案说明

(1)账户 A,这里的余额就是所谓的业务资源,按照前面提到的原则,在第一阶段需要检查并预留业务资源,因此,我们在扣钱 TCC 资源的 Try 接口里先检查 A 账户余额是否足够,如果足够则扣除 30 元。 Confirm 接口表示正式提交,由于业务资源已经在 Try 接口里扣除掉了,那么在第二阶段的 Confirm 接口里可以什么都不用做。Cancel 接口的执行表示整个事务回滚,账户A回滚则需要把 Try 接口里扣除掉的 30 元还给账户。

(2)账号B,在第一阶段 Try 接口里实现给账户 B 加钱,Cancel 接口的执行表示整个事务回滚,账户 B 回滚则需要把 Try 接口里加的 30 元再减去。

方案问题分析

1、如果账户 A 的 Try 没有执行在 Cancel 则就多加了 30 元。

2、由于 Try、Cancel、Confirm 都是由单独的线程去调用,且会出现重复调用,所以都需要实现幂等。

由于 Try、Cancel、Confirm 都是由单独的线程去调用,且会出现重复调用,所以都需要实现幂等。

3、账号 B 在 Try 中增加 30 元,当 Try 执行完成后可能会其它线程给消费了。

4、如果账户 B 的 Try 没有执行在 Cancel 则就多减了 30 元。

问题解决:

1、账户 A 的 Cancel 方法需要判断 Try 方法是否执行,正常执行 Try 后方可执行 Cancel。

2、Try、Cancel、Confirm方法实现幂等。

3、账号 B 在 Try 方法中不允许更新账户金额,在 Confirm 中更新账户金额。

4、账户 B 的 Cancel 方法需要判断 Try 方法是否执行,正常执行 Try 后方可执行 Cancel。

优化方案:

账户 A

try:
    try幂等校验
    try悬挂处理
    检查余额是否够30元
    扣减30元
 
confirm:
    空
 
cancel:
    cancel幂等校验
    cancel空回滚处理
    增加可用余额30元
           

账户 B

try:
    空
confirm:
    confirm幂等校验
    正式增加30元
cancel:
    空
           

总结:

如果拿TCC事务的处理流程与2PC两阶段提交做比较, 2PC通常都是在跨库的DB层面 ,而 TCC则在应用层面的处理 ,需要通过业务逻辑来实现。这种分布式事务的实现方式的 优势 在于,可以让应用自己定义数据操作的粒度,使得 降低锁冲突、提高吞吐量 成为可能。

而 不足之处 则在于对应用的 侵入性非常强 ,业务逻辑的每个分支都需要实现try、confirm、cancel三个操作。此外,其 实现难度也比较大 ,需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。

继续阅读