在前面《从银行转账失败到分布式事务:总结与思考》一文中,已经总结了分布式事务的各种解决方法,以及自己的一点思考。本文算是对上文的一个补充:进一步的思考,主要是这些解决方法在工程上的具体实施。如果后面在工作学习中有新的想法,也会持续更新到本文。
本文地址:http://www.cnblogs.com/xybaby/p/7756163.html
在前文中,已经简要介绍了2PC、TCC、基于异步消息、1PC这几种分布式事务解决办法,这里在补充一种:best effort。
Best Effort
best effort即尽最大努力交付,主要用于在这样一种场景:不同的服务平台之间的事务性保证。比如我们在电商购物,使用支付宝支付;又比如玩网游的时候,通过App Store充值。拿购物为例,电商平台与支付平台是相互独立的,隶属于不同的公司,即使是同一个公司也很可能是独立的部门。因此,这两个平台是不可能使用同一套分布式事务框架的,2PC不行,tcc也不行,异步消息也不行。
其实在上面电商平台与支付平台的例子中,涉及到多重事务性:
电商平台与支付平台之间的事务性:电商的下单操作与支付平台扣款的原子性,不能说支付平台扣了用户的钱,但电商平台不发货;或者说,电商平台先发了货,支付平台没有扣用户的钱;
电商平台内部的事务性:比如订单与优惠券、红包等;
支付平台内部的事务性:比如用户账户、商户账户等;
不管是因为技术原因,还是说安全策略,支付平台只会提供给电商平台一些Http接口,即开放支付服务。电商平台在发出一笔支付请求后,是不大可能立刻获得支付是成功还是失败的确切消息,更多的时候应该是请求已被接受,处理中。这个时候支付平台已经将该请求持久化,保证一定会处理这个请求。当支付平台处理完这个支付请求之后,怎么将结果通知给电商平台呢,要么是电商平台定时轮训,要么是电商平台在初始支付请求的时候携带一个callback,提供给支付平台回调。在这篇文章中提到,支付宝采用的是回调的形式:
“做过支付宝交易接口的同学都知道,我们一般会在支付宝的回调页面和接口里,解密参数,然后调用系统中更新交易状态相关的服务,将订单更新为付款成功。同时,只有当我们回调页面中输出了success字样或者标识业务处理成功相应状态码时,支付宝才会停止回调请求。否则,支付宝会每间隔一段时间后,再向客户方发起回调请求,直到输出成功标识为止。”
这个例子,绘制成流程图就是这样样子的:
再想想上文提到的银行转账的例子,很可能也是采用best effort这种模式,银行之间肯定是相互独立的。首先是本地银行先扣款,然后通知另外一个银行加款,但为什么对方加款失败,没有通知到本地银行,就不清楚了
分布式事务解决方案比较
在这里主要通过以下几个维度来对比分析:
- 一致性
- 资源锁粒度:是否要利用到数据库的锁机制,加锁的粒度
- 子事务串并行:组成 一个事务的多个子事务是并发执行,还是串行执行
- 回滚(补偿):是哪个层面的回滚(补偿)、回滚的代价
注意,上面提到的回滚和补偿是一个意思,“回滚”不局限于DB里面的术语,而是指通用的对某个操作的逆反操作
2PC的强一致性依赖于数据库,而TCC的强一致性依赖于应用层的Commit与cancel。异步消息,1PC,best effort都只保证最终一致性(且最终一致性还可能依赖于人工介入,是否应该算弱一致性?)
2PC需要对整个资源加锁,因此不适用于高并发的分布式场景;而tcc只对需要的资源进行加锁,加锁的粒度小,且try commit Cancel都是本地短事务,因此能在保证强一致性的同时最大化提高系统可用性。而异步消息,1PC,best effort都是先提交一部分事务,无需加锁。
2PC是有数据库来保证回滚,而TCC是应用层实现回滚:为每一个try操作提供一个对应的cancel操作。而异步消息,1PC适用于理论上一定会成功的场景,难以回滚。best effort这种模式,需要服务的调用者实现完整的一个事务操作用于回滚,比如支付失败的情况。数据库的回滚较简单,而应用层的回滚较为困难,更重要的是,回滚也需要作为一个事务进行,部分回滚失败的情况最可怕。
至于子事务的串行、并行,在其他文章中并没有看见过相关讨论,但肯定是实践的时候必须要考虑的问题。即一个分布式事务肯定是由多个分支事务组成,那么多个分支事务是并发执行,还是串行执行呢?特别对于2PC,TCC这些分为多个阶段的解决方案,每个阶段是并发,还是串行呢
分支事务串并行与LPO
首先,对于异步消息,best effort,肯定都是串行的,其中一个分支事务完成之后,再去做另一个分支事务。
但对于2PC,TCC,理论上看起来是并行的,但工程实践中有可以串行。以2PC为例
2PC从介绍的文章来看,多属于并行:即协调者同时让参与者prepare,然后在第二阶段同时通知参与者commit或者abort,下面两个图说明了这个并行的过程。
上面分别是两阶段提交协议成功commit与失败abort的情况,可以看出在prepare阶段,多个参与者是并行的。
而2PC的串行模式,就是说,先通知一个参与者准备,成功的话再通知另一个参与者准备,即准备阶段是串行的。下图来自支付宝:
注意 上面的图示,第二阶段(commit 或者 abort)也画成串行的,这里应该是可以并行的。
那么串行、并行的区别在于哪里呢
(1)并行效率高,整个事务的耗时更少;
(2)而串行在prepare阶段失败的情况下,只需部分回滚;
在工程实践中为什么会采用串行这种方式呢,这是另外一个重要的优化: “最末参与者优化”(Last Participant Optimization,术语来自支付宝),即允许两阶段提交协议中有一个参与者不实现“准备”操作,在其余参与者都prepare ok的情况下,直接提交自己的分式事务。
网络上关于LPO的介绍并不多,在oracle官网Logging Last Resource Transaction Optimization中有如下介绍:
The LLR resource uses a local transaction for its transaction work. The WebLogic Server transaction manager prepares all other resources in the transaction and then determines the commit decision for the global transaction based on the outcome of the LLR resource’s local transaction.
最末参与者优化的原理如下图所示:
本质上,LPO是将最后一个参与者的准备操作与提交/放弃操作合并成一个提交操作,这样提高了分布式事务的执行效率。也可以看到,要使用LPO,在prepare阶段一定是串行的。
对于TCC,流程也是非常类似2PC,即在Try阶段,也可以使用LPO,在《说说分布式事务 》一文中,给出了一个实例的详细流程图。
在一些业务场景,是无需单独的协调者,即事务的发起者同时是组成事务的分支事务。比如支付宝的例子,业务服务和账户服务组成一个分布式事务,在业务服务上发起事务请求,因此没有单独的协调者服务器,使用LPO也比较适合。
再论TCC
前面已经介绍过TCC的三个阶段,Try负责预留资源,Commit提交预留的资源,Cancel“回滚”预留的资源。那么某一个分支事务的Try操作是否可以直接做Commit所做的事情呢,即Try操作直接提交分支事务。在这种情况下,如果所有分支事务的Try阶段都返回OK,那么该分支事务的Commit就什么都不用做,如果需要Cancel,那么就实现回滚。
当然,我看到的更多形式,比如支付宝的XTS,都只是冻结资源:加额外的字段,表明有多少数量的资源处于特殊状态。
我们以一个扣款操作作为分支事务,比如要从账户A扣除100元。如果Try阶段直接执行事务,那么就从A的账户上真正扣除了,而Cancel阶段则加上100,看起来很容易;如果Try阶段只是冻结,那么就会复杂一些,一个可行的方案增加forzen字段的值,同时扣除账户。
但如果考虑加款操作作为分支事务,Try阶段直接执行事务的话,很可能出现cancel阶段钱不够的情况(假设资金不能为负)
因此,个人觉得,TCC框架是不用关心具体形式的,业务只需向框架注册这三个操作就行了,具体怎么操作,完全取决于业务,能满足业务的需求就行。
实践案例
在龙果学院退出的课程《微服务架构的分布式事务解决方案》中,综合运用了各种分布式事务解决方案,如下如所示:
在上图中,使用了三种分布式事务解决办法:
(1)基于可靠消息的最终一致性方案(异步确保型),这个使用比较广,适用于分支事务大概率成功的情况;
上图中使用于:对应支付系统会计异步记账业务,银行通知结果信息存储与驱动订单处理
(2)TCC事务补偿性方案,使用在同时需要保证一致性与高性能的场景
对应上图中支付系统的订单账户操作:订单处理,资金账户处理,积分账户处理
(3)best effort,最大努力通知型方案,适用于跨平台之间的事务原子性保证
对应上图中支付系统的商户业务通知场景