TCC 分布式事务
- T: Try 预处理, 尝试执行,完成所有的业务检查,做好一致性,预留必要的业务资源,做好准隔离性
- C: Confirm 确认,如果所有的分支Try都成功了, 就到了这个阶段, Confirm 是真正执行业务的过程, 不做任何业务检查, 只使用 try 阶段预留的业务资源
- C: Cancel 取消,所有的分支中,如果有一个失败了,则走到 Cancel 的阶段,Cancel 就释放了 Try 阶段预留的业务资源
- 所以,Confirm 和 Cancel 是互斥的,只会执行一个,按照TCC的协议,Confirm 和 Cancel 只返回成功,不返回失败
- 如果有网络问题,或服务器临时故障, 比如崩溃,掉电这种, 事务管理器会进行重试,最终让他成功
- 在 Try 阶段要做业务检查,保障一致性以及资源预留隔离的一些问题
- 此阶段只是一个初步的操作, 它和后续的Confirm一起才能真正构成完整的业务逻辑, 这个算是一部分,当所有的 Try 都完成了,就会向下执行,到Confirm
- Confirm 阶段做一个确认提交, Try 阶段的所有分支事务执行成功后, 开始执行 Confirm, 采用TCC时候,只要Try 成功了,就表示Confirm 阶段不会出错,如果 Confirm 阶段出错,就引入重试机制, 或者人工处理
- 上图是在 Try 阶段, 资源2执行失败,资源1执行成功, 要回滚资源1,这时候资源1要回滚,就是执行 Cancel, 业务执行错误,需要回滚,执行分支的事务的业务取消,预留资源的释放,一般认为Cancel阶段也是一定成功的, 如果Cancel 出错,也是重试机制和人工处理
- 上面全局事务发起方是全局事务的管理器,是独立的第三方,可以实现独立的服务,会生成全局的事务记录,全局的事务id贯穿整个分布式事务的调用链,类似 jaeger 的链路追踪traceID,都是分布式的产品,这些产品设计思路都是相互借鉴,所以,这个全局事务管理器可以追踪和调用状态,由于 Confirm 和 Cancel 失败需要进行重试, 所以要实现幂等性,也就是说同一个操作,无论请求多少次,结果都应该是相同的
- TCC 的三种异常处理:空回滚,幂等,悬挂
- 1 )空回滚:在没有调用 Try 阶段的方法而调用了第二阶段的 Cancel 方法
- Cancel 方法要识别出这是一个空回滚, 然后直接返回成功
- 也就是说空回滚在某种情况下,没有执行 Try, 就直接回滚了
- 2 )幂等:为了保证TCC的二阶段提交重试机制不会引发数据不一致
- 就要求TCC的二阶段Try, Confirm, Cancel 接口保证幂等,这样不会重复使用或者释放资源,如果幂等没有控制做好,就很容易导致数据不一致的严重问题
- 解决思路是增加执行状态,每次执行前,就要查询状态,这就是说你能不能证明你的状态执行到哪里了,可以做一个记录
- 3 )悬挂:对于一个分布式事务,二阶段的Cancel接口比Try接口先执行了
- 悬挂对于分布式的二阶段,Cancel 接口比Try接口先执行
- 对比空回滚是Try是不执行;而悬挂是Cancel先执行,Try再执行
- 它的解决思路是,如果二阶段执行成功,一阶段就不能再继续执行了,在执行一阶段事务时要判断一下,全局事务是否已经有二阶段事务的记录,如果有,就不执行Try了
TCC 为何这么难?
1 ) 从代码上来说
- 可以把我们的TCC放到我们的代码,或者说放到一些场景里来看一下
- 好,我们先从代码说起, 那我们在这里先新建一个目录,就叫 TCC Demo
- 按照TCC的规则来,我们每一个方法函数都需要有这三个函数: Try, Confirm, Cancel
- 比如订单业务,有三个函数
func TryOrder() {}
func ConfirmOrder() {}
func CancelOrder() {}
- 比如下单的时候,需要协同构建库存,增加积分
- 对于库存来说, 有:
func TryStock(){}
func ConfirmStock(){}
func CancelStock(){}
- 对于积分来说,有:
func TryPoint(){}
func ConfirmStock(){}
func CancelOrder(){}
- 如果我们业务中有10个这种,那么我们要写30个这类服务,而且需要有数据库的配合,非常的繁琐
2 ) 基于业务场景来看
- 先看左边的订单服务,它有自己的这个mysql数据库。它第一步先启动事物和这个TCC事务管理器去交互
- 它启动事物之后,第二步就调用这个Try,调用所有的资源上的Try
- 第三步就是要执行提交或者回滚,现在,我们的Try都是ok的
- 下面它就要执行第四步,这第四步的这个Confirm和Cancel是互斥的, 只能择其一执行
- 因为你这个Try执行成功了,就直接都是 confirmStock 和 confirmPoint
- 如果要是有问题,中间可能就是 CancelStock 和 CancelPoint
在实际开发中一般是一个什么样的情况呢?- 就是说 Try 一般是执行主业务的, 比如我们有一个非常难的业务, 在try里基本都搞定了
- 那对于这个 Confirm和Cancel, 它一般都是基本是比较简单的操作
- 其实一般只要Try里没有问题, 那么Confirm和这个Cancel就基本上可以认为是没有太大问题,但是还有其他的问题
- 并不是说,Try ok了,然后 Confirm OK了,Cancel 也 ok 了,就没有问题
- 分布式的复杂,它是存在各种问题的,比如说你这个分布式网络,你永远不要相信分布式里网络永远是好的
- 或者说我现在这个库存有另一个小组维护,这个库存它没有问题,但是我们这个积分服务挂了,或者说很慢
- 我们说这个库存服务,还有这个积分服务,都是其他小组维护的,我们站在我们这个订单角度来说,比如说我是订单服务的负责人
- 那我不能说动人家这个积分服务的这个代码,比如说积分服务,是挂了或者处理很慢
- 就要去看这个TCC的事务管理器了,其实这个TCC在开启事务之前,它会详细的记录日志
- 它有一个日志,每个服务调用了哪个接口,当前事务执行到哪里?
- 比如,即便积分服务挂了,重启了,对吧?重启以后,我们找到日志,找到这个日志仍然可以继续执行
- 假如,我们的积分服务 Cancel 执行这个出错了, 那么我们的TCC事务会一直重试
- 那么TCC事务管理器,要保证执行Cancel不出错,它就会重试。
- 这样一来就可以解决大部分的网络带来的这个问题
- 因为Cancel, Confirm, 这里面的这个业务逻辑基本不是很复杂
- 可能改个状态,或者是添加个什么东西,就是这种类型的,它可能就是网络抖动, 给你造成了一下这个小麻烦
- 那你你这个重试几次之后呢,补偿一下就可以
- 从这儿可以看到TCC事务管理器控制整个流程
- 我们调用TCC,如果Try阶段有问题,那TCC框架就会执行服务的Cancel逻辑撤销之前的操作
- 如果,我们调用这个TCC如果Try阶段它就有问题,比如说你的这个业务太复杂了,在某个地方突然出了一个小bug
- 那么TCC框架就会执行各种Cancel逻辑撤销之前的操作
- 如果上图这个Confirm或Cancel它就是一直不成功,怎么办?
- 其实分布式它本身虽然比较复杂,但是大部分业务的Confirm和Cancel代码就是很简单
- 一般的都是网络问题,通过一直调用的进行补偿就可以了
- 如果一直不成功,你可以在某种程度上做一个记录器进行报警,人为干预
- 自己做的计录器可以按你自己的业务逻辑,因为你这个TCC事务,比如三次或五次都不成功,它要保证成功,则可以通过电话,微信,钉钉通知人报警
- 这时候,比如说开发人员肯定是收到了嘛,人为介入解决这个问题就行了嘛
- 所以,其实TCC真的是不简单,我们只要使用了微服务和这个分布式,就没有办法避免没有这些问题,只要你用了就会遇到这些问题