分布式解决方案有:
最大努力通知型事务
可靠消息一致性事务
TCC事务
TCC是try-confirm-cancel的单词首字母缩写,是一个类2PC的柔性事务解决方案,由支付宝提出后得到广泛的实践。
主服务调用两个从服务,这两个从服务属于不同的进程,各自操作不同的数据库表。主服务A调用从服务B后,继续调用从服务C,这个过程要保证调用B、C同时成功,同时失败。如果任何一个服务的操作失败了,就全部一起回滚,撤销已经完成的操作。
那么如何保证多个服务的调用是同时成功、同时失败呢。TCC帮我们实现了这个目标。
TRY阶段
首先进行TRY阶段,该阶段主要做资源的锁定/预留,设置一个预备的状态,冻结部分数据,等等。
比如:电商平台先在订单模块做下单操作,下单成功后调用库存模块做扣减库存,扣减成功调用支付接口进行支付,然后调用积分模块做积分的增加,最后调用发货模块做发货处理。
这个过程中的Try阶段描述如下:
订单服务先做下单操作,这是个本地事务,能够保证ACID的事务特性。下单成功后,订单服务将当前订单状态由初始化改为处理中进行扣库存操作,这里不能直接将库存扣除,应当冻结库存,将库存减去后,将减去的值保存在已冻结的字段中。
例如:本来库存数量是100,要减去5个库存,不能直接100 - 5 = 95,而是要把可销售的库存设置为:100 - 5 = 95,接着在一个单独的库存冻结的字段里,设置一个5。也就是说,有5个库存是给冻结了。
此时订单状态为OrderStatus.DEALING。
接着进行支付操作。那么。为什么不直接进行支付,然后改为支付完成呢?因为存在支付失败甚至支付未知的风险,只要进行了支付操作,订单状态就不是初始化了。也就是说,不能直接把订单状态修改为已支付的确认状态!而是应当先把订单状态修改为DEALING,也就是处理中状态。该状态是个没有任何含义的中间状态,代表分布式事务正在进行中。
积分服务的增加积分接口也是同理,不能直接给用户增加会员积分。可以先在积分表里的一个预增加积分字段加入积分。
比如:用户积分原本是1000,现在要增加100个积分,可以保持积分为1000不变,在一个预增加字段里,设置一个100,表示有100个积分准备增加。
发货服务的发货接口也是同理,可以先创建一个发货订单,并设置这个销售出库单的状态是“DEALING”。
也就是说,刚刚创建这个发货订单,此时不能确定他的状态是什么。需要等真实发货之后再进行状态的修改。
这整个过程也就是所谓的TCC分布式事务中的TRY阶段。
简而言之,TRY阶段的业务的主流程以及各个接口提供的业务含义,不是直接完成那个业务操作,而是完成一个资源的预准备的操作,状态均为过渡态。
CONFIRM阶段
常见的TCC框架,如:ByteTCC、tcc-transaction 均为我们实现了事务管理器,用来执行CONFIRM阶段。他们能够对各个子模块的try阶段执行结果有所感知。
感知各个阶段的执行情况以及推进执行下一个阶段的操作较为复杂,不太可能自己手写实现,我们最好是借助开源框架实现。
为了实现这个阶段,我们需要加入CONFIRM操作相关的代码做事务的提交操作。
接着上述的情景来说:
订单服务中的CONFIRM操作,是将订单状态更新为支付成功这样的确定状态。
库存服务中,我们要加入正式扣除库存的操作,将临时冻结的库存真正的扣除,更新冻结字段为0,并修改库存字段为减去库存后的值。
同时积分服务将积分变更为增加积分之后的值,修改预增加的值为0,积分值修改为原值+预增加的100分的和。
发货服务也类似,真实发货后,修改DEALING为已发货。
当TCC框架感知到各个服务的TRY阶段都成功了以后,就会执行各个服务的CONFIRM逻辑。
各个模块内的TCC事务框架会负责跟其他服务内的TCC事务框架进行通信,依次调用各服务的CONFIRM逻辑。正式完成各服务的完整的业务逻辑的执行。
CANCEL阶段
CONFIRM是业务正常执行的阶段,那么异常分支自然交给CANCEL阶段执行了。
接着TRY阶段的业务情景来说。
订单服务中,当支付失败,CANCEL操作需要更改订单状态为支付失败
库存服务中的CANCEL操作要将预扣减的库存加回到原库存,也就是可用库存=90+10=100
积分服务要将预增加的100个积分扣除
发货服务的CANCEL操作将发货订单的状态修改为发货取消
当TCC框架感知到任何一个服务的TRY阶段执行失败,就会在和各服务内的TCC分布式事务框架进行通信的过程中,调用各个服务的CANCEL逻辑,将事务进行回滚。
思考
上述我们举的例子基本描述了一个TCC的执行过程,可以看出:
TCC分布式事务的核心思想,就是当系统出现异常时,比如某服务的数据库宕机了;某个服务自己挂了;系统使用的第三方服务如redis、elasticsearch、MQ等基础设施出现故障了或者某些资源不足了,比如说库存不够等等情况下,先执行TRY操作,而不是一次性把业务逻辑做完,进行预操作,看各个服务能不能基本正常运转,能不能预留出需要的资源。
如果TRY阶段均执行ok,即,数据库、redis、elasticsearch、MQ都是可以写入数据的,并且保留成功需要使用的一些资源(比如库存冻结成功、积分预添加完成)。
接着,再执行各个服务的CONFIRM逻辑,基本上CONFIRM执行完成之后就可以很大概率保证一个分布式事务的完成了。
那如果TRY阶段某个服务就执行失败了,比如说底层的数据库挂了,或者redis挂了,那么此时就自动执行各个服务的CANCEL逻辑,把之前的TRY逻辑都回滚,所有服务都不执行任何设计的业务逻辑。从而保证各个服务模块一起成功,或者一起失败。
到这里还是不能保证完全的事务一致性,试想,如果真的发生服务突发性宕机,比如订单服务挂了,那么重启之后,TCC框架如何保证之前的事务继续执行呢?
这个其实不必担心,成熟的TCC框架比如TCC-transaction中引入了事务的活动日志,它们保存了分布式事务运行的各个阶段的状态。后台会启动一个定时任务,周期性的扫描未执行完成的事务进行重试,保证最终一定会成功或失败。
这里也体现了TCC解决方案是一个保证最终一致性的柔性事务解决方案。
严格遵守ACID的分布式事务我们称为刚性事务,而遵循BASE理论(基本可用:在故障出现时保证核心功能可用,软状态:允许中间状态出现,最终一致性:不要求分布式事务打成中时间点数据都是一致性的,但是保证达到某个时间点后,数据就处于了一致性了)的事务我们称为柔性事务,其中TCC编程模式就属于柔性事务
TCC编程模式本质上也是一种二阶段协议,不同在于TCC编程模式需要与具体业务耦合,下面首先看下TCC编程模式步骤:
所有事务参与方都需要实现try,confirm,cancle接口。
事务发起方向事务协调器发起事务请求,事务协调器调用所有事务参与者的try方法完成资源的预留,这时候并没有真正执行业务,而是为后面具体要执行的业务预留资源,这里完成了一阶段。
如果事务协调器发现有参与者的try方法预留资源时候发现资源不够,则调用参与方的cancle方法回滚预留的资源,需要注意cancle方法需要实现业务幂等,因为有可能调用失败(比如网络原因参与者接受到了请求,但是由于网络原因事务协调器没有接受到回执)会重试。
如果事务协调器发现所有参与者的try方法返回都OK,则事务协调器调用所有参与者的confirm方法,不做资源检查,直接进行具体的业务操作。
如果协调器发现所有参与者的confirm方法都OK了,则分布式事务结束。
如果协调器发现有些参与者的confirm方法失败了,或者由于网络原因没有收到回执,则协调器会进行重试。这里如果重试一定次数后还是失败,会怎么样那?常见的是做事务补偿。
蚂蚁金服基于TCC实现了XTS(云上叫DTS),目前在蚂蚁金服云上有对外输出,这里我们来结合其提供的一个例子来具体理解TCC的含义,以下引入蚂蚁金服云实例:
“首先我们假想这样一种场景:转账服务,从银行 A 某个账户转 100 元钱到银行 B 的某个账户,银行 A 和银行 B 可以认为是两个单独的系统,也就是两套单独的数据库。
我们将账户系统简化成只有账户和余额 2 个字段,并且为了适应 DTS 的两阶段设计要求,业务上又增加了一个冻结金额(冻结金额是指在一笔转账期间,在一阶段的时候使用该字段临时存储转账金额,该转账额度不能被使用,只有等这笔分布式事务全部提交成功时,才会真正的计入可用余额)。按这样的设计,用户的可用余额等于账户余额减去冻结金额。这点是理解参与者设计的关键,也是 DTS 保证最终一致的业务约束。”
在try阶段并没有对银行A和B数据库中的余额字段做操作,而是对冻结金额做的操作,对应A银行预留资源操作是对冻结金额加上100元,这时候A银行账号上可用钱为余额字段-冻结金额;对应B银行的操作是对冻结金额上减去100,这时候B银行账号上可用的钱为余额字段-冻结金额。
如果事务协调器调用银行A和银行B的try方法有一个失败了(比如银行A的账户余额不够了),则调用cancle进行回滚操作(具体是对冻结金额做反向操作)。如果调用try方法都OK了,则进入confirm阶段,confirm阶段则不做资源检查,直接做业务操作,对应银行A要在账户余额减去100,然后冻金额减去100;对应银行B要对账户余额字段加上100,然后冻结金额加上100。
最关心的,如果confirm阶段如果有一个参与者失败了,该如何处理,其实上面操作都是xts-client做的,还有一个xts-server专门做事务补偿的。
三、总结
TCC是对二阶段的一个改进,try阶段通过预留资源的方式避免了同步阻塞资源的情况,但是TCC编程需要业务自己实现try,confirm,cancle方法,对业务入侵太大,实现起来也比较复杂。