编写自己的分布式框架—事务解决方案(二)
前言
工欲善其事必先利其器,既然我们决定要做一个分布式事务框架,那首先需要了解一下,分布式事务是怎么回事,它跟传统的本地事务有什么区别,解决方案有哪些,每种解决方案的对比等等。
本地事务
在了解分布式事务之前,先回顾一下本地事务,顾名思义,本地事务就是在同一个JVM中,一个开启了事务的业务方法就是本地事务。而这一个开启了事务的业务方法里面的操作要么全部执行成功,要么全部执行失败,不允许只成功一半另外一半执行失败的事情发生。例如该业务方法中,有两次数据库更新操作,那么这两次数据库操作要么全部执行成功,要么全部回滚。使用专业术语来讲的话,就是事务的4个基本特性:Atomicity(原子性)、Consistency(一致性)、Isolation(隔离性)、Durablity(持久性),统称ACID,这里简单的对ACID做一个概念的说明,当作是做个笔记:
· Atomicity(原子性)
是指事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。通俗的说,就是所有操作要么全部成功,要么全部失败回滚。
· Consistency(一致性)
是指事务执行前后,数据从一个状态到另一个状态必须是一致的,比如A向B转账(A、B的总金额就是一个一致性状态),不可能出现A扣了钱,B却没收到的情况发生。
· Isolation(隔离性)
是指当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。这里涉及到数据库的隔离级别的概念,不是我们讨论的主题,不详细展开,大家可以自行查阅相关资料。
· Durablity(持久性)
是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
分布式事务
通过以上的回顾我们知道,本地事务对于我们来说不是什么问题,因为我们可以直接使用数据库的事务支持,比如mysql、oracle这些数据库对事务都有很好的支持。但是,对于分布式应用来说的话,事务就没有那么简单了,因为需要开启事务的业务方法,很可能是分布在不同应用程序中,这就说明,大家不在同一个JVM中,事务空间都不一样了,那就没办法做到要么全部执行成功,要么全部执行失败了,我们可以看以下分析图:
image.png
如上图所示:这是一个分布式应用,完成一个付款业务的操作需要有4个微服务参与,大家都独立运行在自己的JVM中,其中,订单系统、商品系统和会员系统是服务提供者,有自己对应的数据库。
客户端应用在发起了付款请求时,调用了订单系统的支付业务makePayment,而makePayment方法中,又调用了远程商品系统的decrease方法和远程会员系统的payment方法,分别做减库存和扣余额的操作,那现在就有两种情况了:
第一种情况是流程正常执行,各个业务参与者都没有异常,万事大吉。
第二种情况是其中有一个业务参与者出现异常了,按照我们上面对本地事务的理解,它们应该要做到要么全部执行成功,要么全部执行失败了,这样才能确保数据一致性。但现在这个不是单体应用,而是分布式应用,原本一个本地逻辑执行单元被拆分到了多个独立的微服务中,这些微服务又分别操作不同的数据库和表,服务之间通过网络调用。所以这就没办法确保要么全部执行成功,要么全部执行失败了,因为我怎么知道远程的服务是否执行成功呢。
所以,现在的问题就出现了,我们不能再用单体应用的那种事务方式,套用在分布式应用中了,必须要思考用什么样的解决方案来控制分布式应用的事务问题。这就是“分布式事务”。
分布式事务的解决方案
分布式事务的解决方案有很多,但可以具体归纳为两种:
● 强一致性事务
强一致性事务的代表就是XA事务协议了,它由Oracle Tuxedo提出,把分散到各个JVM中的事务资源,又整合为全局事务统一管理了。
● 柔性事务
而柔性事务并没有像强一致性事务那样整合为全局事务,但其目的 都是确保分布式事务最终一致性的。柔性事务又可以细分为TCC事务和可靠消息事务,这两种分布式事务处理方式不一样,接下来我们具体分析一下这几种分布式事务解决方案的实现原理。
XA事务协议
XA是一个分布式事务协议,XA中大致分为两部分:事务管理器和本地资源管理器。其中本地资源管理器往往由数据库实现,比如Oracle、DB2这些商业数据库都实现了XA接口,MySQL5.7之后也支持XA分布式事务。而事务管理器作为全局的调度者,负责各个本地资源的提交和回滚。
XA分布式事务协议的工作原理是:把各个微服务中的本地资源交给一个统一的事务管理器管理,事务管理器可以看做是事务协调者的角色(coordinator),各个本地资源管理器可以看做是事务参与者的角色(partcipant)。各个事务参与者之间不能直接通讯,而是通过事务协调者间接通讯,通俗来说,服务A怎么知道服务B是否执行成功?就是由事务协调者转告各个事务参与者了。通过以下分析图,看看XA实现分布式事务的流程:
image.png
image.png
以上就是XA分布式事务执行流程,加入了全局的事务管理器作为协调者,在接收到发起带事务的业务方法后,发送prepare到各个事务参与者,各个事务参与者接收到prepare后,开启本地事务being,并执行本地业务流程,如果流程正常运行,则返回ready结果给事务协调者,告知准备就绪了,这时,如果各个事务参与者返回的结果都是ready,那么事务协调者就会再次发送一个全局事务提交global_commit的消息到各个事务参与者,最后,各个事务参与者受到global_commit后,提交本地事务commit。
这是业务流程正常执行的情况,那如果因为流程有异常就走如下流程:事务协调者还是发送prepare到各个事务参与者,事务参与者接收到prepare后,开启本地事务begin,接着执行业务流程,此时,如果有某个事务参与者执行业务报错,返回异常abort给事务协调者,事务协调者会再次发送全局回滚global_rollback给各个事务参与者,事务参与者接受到global_rollback后,开始回滚本地事务rollback,即便是流程正常执行的也要回滚掉,这样就能确保要么一起成功,要么一起失败。
总的来说,XA协议比较简单,而且一旦商业数据库实现了XA协议,使用分布式事务的成本也比较低。但是,XA也有致命的缺点,那就是性能不理想,特别是在并发量很高的情况下,会带来性能瓶颈,因为根据以上执行流程图的分析可知,在全局事务管理器向各个事务参与者发送prepare时,是需要锁住资源的,也就是此时,所有相关连的微服务都处于阻塞状态,需要等到所有事务参与者返回最终处理结构,才能释放锁,所以XA无法满足高并发场景。其次,XA目前在数据库的支持上不太理想,mysql5.7之前是不支持的,并且还有许多nosql也没有支持XA,而大多数新型的互联网微服务应用都会使用各种nosql数据库,所以这就导致了XA的应用场景变得非常狭隘。
TCC事务
TCC是Try-Confirm-Cancel的简称。其核心思想是:每个需要开启分布式事务的业务方法,都要注册一个与其对应的检测、确认和撤销的操作,如下:
● Try阶段:主要是对业务系统做检测的操作,没有问题就调用确认操作,有问题则调用取消操作。
● Confirm阶段:确认执行业务操作。
● Cancel阶段:取消执行业务操作。
具体执行流程如下:
image.png image.png
通过以上的流程,我们可以发现,TCC的分布式事务处理与XA的分布式事务处理流程是非常相似的, 调用try接口检查业务是否有异常的操作,类似于XA的prepare预提交,如果接口返回正常,则调用confirm确认执行业务,操作数据。
那如果其中有一个事务参与者在调用了try接口检测后,返回了异常给事务协调者,但是之前很可能已经有其他事务参与者调用了confirm接口,执行业务流程操作了数据,那这时,事务协调者就需要调用事务参与者的cancel接口,撤销之前修改的数据,达到类似回滚的效果。
不过XA是在跨库的DB层面,而TCC是应用层面,需要通过业务逻辑来实现分布式事务。TCC的实现方式优势在于:没有项目XA协议那样,把分布的资源统一管理,这就使得分布的资源不会被加锁,从而提高整体的吞吐量,所以这种分布式事务的解决方案,在性能和吞吐量要求高的应用使用的还是比较多的。而不足之处则在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现try、confirm、cancel三个操作。此外,其实现难度也比较大,需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。同时,confirm和cancel接口还要考虑幂等性的问题,因为confirm和cancel有可能会被多次调用。
可靠消息事务
可靠消息事务全称叫做可靠消息事务最终一致性,它通常没有像前面两种分布式事务解决方案那样有回滚或撤销数据的操作,而更多的是强调事务的补偿和重试。这里的可靠消息指的是消息队列中间件,消息队列其中一个特点就是消息可靠性,而该解决方案最终能达到事务一致,依靠的核心就是消息队列。先粗略的看看它执行流程:
image.png
从以上的执行流程可以发现,各个事务参与者都是相对独立的,不管在执行业务方法的过程中是否有异常,整体的业务流程都要先跑完,这个是该解决方案的前提。然后在调用事务参与者的业务方法的同时,往消息队列发送事务相关的消息,这样的话,出现异常的事务参与者再从消息队列中获取消息,重新执行本地的业务,达到补偿和重试的效果,整个事务中,不管中间有哪些参与者出错,但是最终还是事务一致的。
这种解决方案是所有解决方案中最柔性的,并且灵活度非常高,可以根据自己具体的业务场景做改变,同时,对比TCC来说,性能和吞吐量更高,并且对应用的侵入性更低。性能的提高体现在:没有了业务检测的环节,跟原本一样,该怎么调用远程方法就怎么调用,只是增加了一个往MQ发送消息的操作,但是该操作是异步的,而且MQ也是具备高吞吐量的特性。而侵入性更低体现在:MQ是中间件,只需要通过网络来调用即可,我们的业务方法并不会由于分布式事务解决方案的加入而有太多的改造,加入的代码更多是以外围扩展,或者组件的方式加入。
可靠消息事务最终一致性的解决方案优点很明显,但缺点也不是没有的,首先该解决方案设计过于复杂,组件很多,需要考虑的情况繁杂,实现起来比较困难。其次,虽然它是最柔性,最灵活和性能最高的,但是事务的原子性和一致性是最弱的,因为这种解决方案是以大家都不出错为前提的,如果其中有一个出错,自己通过补偿机制重新执行本地事务,但重试的过程本身就是不确定性的,比如说:A转钱给B,A账户扣钱了,但是B账户加钱时出错,在该机制下,A账户不会回滚,而是让B账户重新尝试加钱,那这就产生时间上的延迟了,很可能等了很久,都没见B账户把钱加上去,或者不断重试都是失败的,最终导致整个事务不一致,需要人工处理。
总的来说,可靠消息事务最终一致性由于它的事务原子性和一致性比较弱,所以决定了它在一些事务ACID要求非常强的应用中是不能使用的,否则会造成很多安全性的问题,但是对于大多数互联网应用来说,都是一个不错的解决方案。而具体使用哪一种,还是取决于项目的业务场景,然后做全面的对比、考量和取舍。