背景

随着微服务的普及,分布式事务成为了系统设计中不得不面对的一个问题,而分布式事务的实现则十分复杂。阅读本文之前,需要你对数据库事务的ACID、CAP理论、Base理论以及两阶段提交有一定的认知,不熟悉者请自行百度或者阅读参考博客1、2、3和4。除此之外,在阅读本文过程中,如果对某种方案不理解,强烈建议先阅读对应方案中的参考博客后再阅读本文中对应的介绍。

为了便于后文叙述,这里对ACID中的C(一致性)做一个强调:严格的事务一致性是使数据库从一个一致性状态变到另一个一致性状态,且事务中间状态不能被观察到。

分布式事务的七种实现方案:

1、基于可靠消息服务(基于可靠消息中间件);

2、最大努力尝试(基于消息中间件);

3、TX-LCN(对LCN的实现);

4、X/Open DTP模型(XA规范,基于两阶段提交);

5、阿里DTS(基于TCC);

6、华为ServiceComb(对SAGA模式的实现);

7、阿里GTS(开源产品为Fescar,对XA协议改进后的实现)。

1、基于可靠消息服务

我们常见的消息中间件比如LinkedLin公司的Kafka、Rabbit科技有限公司的RabbitMQ以及Apache提供的ActiveMQ等等,都不支持事务。而阿里提供的RocketMQ则在解决了消息的顺序性和重复消息幂等性的基础上,实现了对事务的支持(详见参考博客5)。因而基于RocketMQ,就可以实现分布式事务(详见参考博客6)。实际上,参考博客6中给出了两套基于消息服务的实现方案:第一,基于本地消息服务的方案,针对的是消息中间件本身不支持事务的场景,需要从应用设计的角度实现消息数据的可靠性;第二,基于独立消息服务的方案,针对的是消息中间件本身支持事务的场景。为了便于后面对比分析,这里贴出了基于独立消息服务的设计方案:

基于可靠消息服务的分布式事务方案的核心原理是对发送到消息中间件的消息进行“两阶段提交”,即先提交,执行完本地事务后再确认消息。通过这样的机制,给事务的回滚提供了可能。

如下图所示。如果用ACID来衡量该方案,基于可靠消息服务的分布式事务方案能保证事务的最终原子性和持久性,但无法保证一致性和隔离性。这里在原子性前面加“最终”二字,是因为基于消息的操作本质上属于异步操作,显然是非实时的。持久性自然不必多说。那么为什么说该方案无法保证一致性和隔离性呢?由下图可知,本地事务执行提交成功之后,才会对消息进行确认,而这个时候,远程事务还未提交,一致性显然无法满足。

我们知道,数据库的隔离性是通过锁机制来保证的,因而基于可靠消息服务的分布式事务方案要想满足隔离性,往往还需要在事务发起方采用分布式锁机制。因而总的来说,基于可靠消息服务的分布式方案适用于对业务的实时一致性以及事务的隔离性要求都不高的内部系统。

2、最大努力尝试

最大努力尝试方案和基于可靠消息服务一样,都依赖于消息中间件(参考博客7)。不同的是,消息中间件不需要保证可靠性,分布式事务的实现是依靠额外的校对系统或者报警系统(报警后人工处理)来保障的。因而和基于可靠消息服务一样,最大努力尝试的分布式方案只能保证事务的最终原子性和持久性,无法保证一致性和隔离性。在应用场景方面,正如参考博客7中所说,最大努力尝试方案常常用于对业务的实时一致性以及事务的隔离性要求都不高的内部系统或者跨企业的业务活动中。下图为最大努力尝试的方案:

同基于可靠消息服务的方案一样,最大努力尝试方案能保证事务的最终原子性和持久性,但无法保证一致性和隔离性。尽管最大努力尝试方案只能保证最终原子性和持久性,但因其实现十分简单,常常成为企业很多非核心业务的首选方案。

3、TX-LCN

由LCN的官网(参考博客8)可知,LCN是lock、confirm和notify三个单词的缩写。个人人为,LCN方案是本文中七个方案中除了刚刚已经介绍的两个基于消息中间件的方案之外,最容易理解的方案,因而将其排在第三位来介绍。正如其官网文档(详见参考博客8)所说,LCN并不产生事务,LCN只是本地事务的协调工。这就意味着,使用LCN的系统完全依赖于本地事务。遗憾的是,LCN的官网对其核心原理介绍得比较简略,看完不得其要领,反而是在另外的博客(参考博客9和10)中相对详细地介绍了其实现原理。

LCN中包含一个TxManager和一个TxClient。其中TxManager负责维护全局的事务信息,而TxClient位于业务模块和本地事务层之间,其作用是代理本地事务层,通过代理连接词实现了javax.sql.DataSource接口,并重写了close方法,事务模块在提交关闭以后TxClient连接池将执行"假关闭"操作,等待TxManager协调完成事务以后在关闭连接,如下图所示。

LCN的核心步骤

1. 创建事务组

是指在事务发起方开始执行业务代码之前先调用TxManager创建事务组对象,然后拿到事务标示GroupId的过程。

2. 添加事务组

添加事务组是指参与方在执行完业务方法以后,将该模块的事务信息添加通知给TxManager的操作。

3. 关闭事务组

是指在发起方执行完业务代码以后,将发起方执行结果状态通知给TxManager的动作。当执行完关闭事务组的方法以后,TxManager将根据事务组信息来通知相应的参与模块提交或回滚事务。

以数据库为例,前面提到的“假关闭”就是指步骤2中执行完业务方法后调用的close()方法是覆写后的方法,该方法并不会真正提交事务,而是一直持有数据库连接,并持有事务所需的相关资源锁,直到第3步,由TxManager异步通知事务提交或回滚才释放锁。

以参考博客10中的时序图为例(见下图),参与方A和参与方B在执行完业务操作后,事务实际上并未提交,而是将提交前本地事务的操作结果返回给事务发起方,由事务发起方通知TxManager,并由后者根据通知的结果来异步通知各参与方最终提交或回滚事务。当然TxManager对参与方的通知可能会失败,因而需要补偿机制。

由此可知,LCN中的三个单词对应了LCN分布式事务操作中的三个关键步骤:1、分布式事务操作前先锁定(lock)所有资源直到异步通知(notify)释放资源;2、执行业务操作,根据操作结果确认(confirm)事务应该提交还是回滚;3、根据第2步中的操作结果异步通知(notify)事务的提交或回滚并最终释放资源。

至此,我们了解到,LCN的核心原理是通过协调本地事务来实现分布式事务,分布式事务的实现依赖于本地事务。因而基于LCN的分布式事务的ACID特性取决于本地事务的ACID特性。一般来说,如果本地事务都能保证ACID,那么基于LCN的分布式事务也能满足AID。而对于一致性(Consistency),这是分布式事务的一个通病。

由Base理论可知,对于分布式系统,我们更关注的是最终一致性和最终一致性的实时性。相对于前面介绍的基于消息中间件的两种方案来说,基于LCN的分布式事务方案最终一致性的实时性远高于前两者。当然,其代价是并发性能的极大降低。实际上,对于分布式系统而言,能做到准实时的最终一致性就已经能满足绝大多数应用场景。对于像银行业务等对一致性有极致要求的极端场景,还可以通过在业务系统中使用分布式锁或者分布式队列来保证。

小结:LCN方案相对于后面将要介绍的其他方案来说,其优点是实现相对简单,但其缺点也是显然的:第一,依赖本地事务,如果要操作的资源不支持本地事务,则LCN模式无法直接使用。当然,对于这个限制,新版的TX-LCN通过支持后面将会介绍的TCC模式和TXC模式来解决。第二,LCN事务提交的整个过程都需要锁住资源,因而性能低于TCC和TXC(TCC和TXC就是为了缩短分布式事务操作过程中资源的锁定时间)。

4、基于XA规范的X/Open DTP模型

X/Open,即现在的open group,是一个独立的组织,主要负责制定各种行业技术标准。X/OpenDTP即为该组织制定的一套分布式事务的方案。关于该模型详细的介绍请阅读参考博客11,这里只介绍其最核心的原理。

DTP模型元素

应用程序(Application Program ,简称AP):用于定义事务边界(即定义事务的开始和结束),并且在事务边界内对资源进行操作。

资源管理器(Resource Manager,简称RM):如数据库、文件系统等,并提供访问资源的方式。

事务管理器(Transaction Manager ,简称TM):负责分配事务唯一标识,监控事务的执行进度,并负责事务的提交、回滚等。

通信资源管理器(Communication Resource Manager,简称CRM):控制一个TM域(TMdomain)内或者跨TM域的分布式应用之间的通信。

通信协议(Communication Protocol,简称CP):提供CRM提供的分布式应用节点之间的底层通信服务。

这里我们重点看看AP、TM和RM之间的关系:

由上图可知,XA规范的最主要作用是,定义了RM-TM的交互接口。实际上,XA规范除了定义的RM-TM交互的接口之外,还对两阶段提交协议进行了优化。具体的优化包括只读断言和一阶段提交。具体请阅读参考博客11。

接下来我们来看XA规范的具体工作原理。参考博客14中给出了XA规范提交流程示意图:

提交步骤为:

在开始一个全局事务之前,涉及的RM必须通过ax_regr(),向TM注册以加入集群;对应的,在没有事务需要处理的时候,RM可以通过ax_unreg()向TM要求注销,离开集群。

TM在对一个RM执行xa_开头的具体操作前,必须先通过xa_open()打开这个RM(本质是建立对话)——这其实也是分配XID的一个行为;与之相应的,TM执行xa_close()来关闭RM。

TM对RM调用的xa_start()和xa_stop()这对组合,一般用于标记局部事务的开头和结尾。这里需要注意的有三点:

对于同一个RM,根据全局事务的要求,可以前后执行多对组合——俾如说,先标记一个流水账INSERT的局部事务操作,然后再标记账户UPDATE的局部事务操作。

TM执行该组合只是起到标记事务的作用,具体的业务命令是由AP交给RM的。

该组合除了执行这些标记工作外,其实还能在RM中实现多线程的join/suspend/resume管理。

TM调用RM的xa_prepare()来进行第一阶段,调用xa_commit()或xa_rollback()执行第二阶段。

这里需要强调的是,XA规范中,整个两阶段提交过程资源都是处于锁定状态(见下图)。在下图中,无论Phase2的决议是commit还是rollback,事务性资源的锁都要保持到Phase2完成才释放。由此可知,基于XA规范的X/OpenDTP和LCN都存在资源长期锁定的问题。

 

小结:X/Open DTP的核心原理是基于两阶段提交(XA规范),通过在整个提交过程中对资源进行锁定来实现分布式事务,能很好地满足AID特性,以及准实时的最终一致性。此外,该方案同LCN类似,性能低于TCC和TXC。因而在实际应用中,较少有系统会选择该方案。

5、基于TCC的支付宝DTS

关于TCC原理,强烈建议先阅读参考博客12和13。这里给出参考博客13中的示意图:

TCC 分布式事务模型包括三部分:

主业务服务:主业务服务为整个业务活动的发起方,服务的编排者,负责发起并完成整个业务活动。

从业务服务:从业务服务是整个业务活动的参与方,负责提供 TCC 业务操作,实现初步操作(Try)、确认操作(Confirm)、取消操作(Cancel)三个接口,供主业务服务调用。

业务活动管理器:业务活动管理器管理控制整个业务活动,包括记录维护 TCC 全局事务的事务状态和每个从业务服务的子事务状态,并在业务活动提交时调用所有从业务服务的Confirm 操作,在业务活动取消时调用所有从业务服务的 Cancel 操作。

一个完整的 TCC 分布式事务流程如下:

1. 主业务服务首先开启本地事务;

2. 主业务服务向业务活动管理器申请启动分布式事务主业务活动;

3. 然后针对要调用的从业务服务,主业务活动先向业务活动管理器注册从业务活动,然后调用从业务服务的 Try 接口;

4. 当所有从业务服务的 Try 接口调用成功,主业务服务提交本地事务;若调用失败,主业务服务回滚本地事务;

5. 若主业务服务提交本地事务,则 TCC 模型分别调用所有从业务服务的 Confirm 接口;若主业务服务回滚本地事务,则分别调用Cancel 接口;

6. 所有从业务服务的 Confirm 或 Cancel 操作完成后,全局事务结束。

小结:以ACID来衡量TCC可知,TCC能满足AID特性,以及准实时的最终一致性。TCC的核心原理是,在分布式事务操作中,先将所需资源预占,然后将锁释放,最后再根据资源预占的情况来决定使用资源还是退回资源。相比于XA规范,TCC方案采用预留资源的方式,将两阶段提交过程中的全局锁分成了两段本地事务锁,缩短了分布式资源锁定的时间,从而提高了事务的并发度。相对于后面即将介绍的SAGA而言,因为TCC采用预占资源的方式,其补偿动作实现比较简单。当然,TCC的缺点是业务入侵性大,尤其是已有业务如果想使用TCC方案,就需要修改原来的业务逻辑(后面介绍SAGA时还会再强调这一点)。

6、基于SAGA的华为ServiceComb

saga是1987年的一篇数据库论文里提到的一个概念(参考博客16~19,重点是16),而ServiceComb是华为一个项目组对saga模式的实现方案。论文中指出,一个Saga事务就是一个Long Live Transaction(LLT),而一个LLT可以分解为多个本地事务所组成,即LLT= T1+ T2 + ... + Tn。每个本地事务执行完提交之后就会立即生效,比如执行完T1和T2之后,在执行T3时,T1和T2的事务就已经提交了。

假如执行T3失败,由于T1和T2已经提交,无法回滚了。为了解决这个问题,saga要求业务服务方对每个本地事务Tx都提供对应的反向补偿操作Cx(反向补偿是指Tx的逆向操作),反向补偿操作也是执行完就立即生效。在执行一个LLT的过程中,如果任意一个Tx出错了,则通过调用所有已执行的Tx对应的Cx来进行反向补偿。比如执行完T1和T2之后,执行T3时出错,则需要执行C3、C2和C1来进行补偿。

论文中对被saga调用的服务提出了两点要求:其一是被调用的服务要支持幂等。由于分布式服务一定存在网络超时,所以这一点对于分布式服务来说,一般都能满足。其二是服务要满足可交换补偿。如图所示:

 

这里有必要对可交换补偿做一下说明。其实服务支持幂等和服务满足可交换补偿这两点要求是为了处理执行补偿操作时必然会遇到的两个细分场景。我们首先来说说执行补偿操作时会遇到的哪两个场景:第一,事务操作Tx根本未被执行(比如因网络丢包导致事务操作Tx在图中被丢弃,未到达服务端,或者在服务端执行失败)。

对于这种场景,Cx无须也不能执行,否则反而会出错,也就是说,saga执行Cx操作的前提是服务方确实执行了Tx操作(成功或失败均可);第二,如上图中的右图所示,事务操作T发出后事务操作在网络中出现了较大的延时,触发了saga的超时机制,因而执行了正向重试,发现重试失败,接着执行反向补偿操作C,当C已经执行完之后,最开始的事务操作T才到达服务方。

这个时候,saga要求最开始的事务操作T不能生效,因为C已经生效了。也就是说,saga要求服务一旦接受并执行了反向补偿操作C,则不会再处理与之对应的正向操作。这么做的目的是为了防止先到达的反向补偿操作被后到达的正向事务操作给覆盖掉。

其实场景二的实现依赖场景一中提供的保障,因为示例中正向的重试操作也有可能无法到达服务端,这就变成了场景一了。其实这个问题在后面介绍的TCC方案中也会遇到,并需要被解决(有兴趣可提前看参考博客20)。

除此之外,参考博客16中还说明了saga模式本身只支持ACID中的ACD,而无法支持隔离性(而如果用本文的评判标准来看,saga也无法只能满足准实时的一致性,而无法满足强一致性)。为了支持隔离性,需要考虑在业务层加入锁机制或者类似TCC的方式,采用在业务层预先冻结资源的方式对资源进行隔离。

关于saga模式的隔离性我还想补充说明的是,因为saga模式的本地事务是执行完就立即提交,而不能回滚,那么在没有隔离性保障的情况下,反向补偿操作很可能无法执行成功。比如说有A和B和C三个账户,账户余额都为100元,执行如下两个并发事务:

小结:saga模式的核心原理是,将一个全局事务分成若干个能独立提交的本地事务,每个本地事务都对应一个反向补偿操作,当本地事务提交失败后,通过反向补偿操作来取消本地事务的影响。

相比于TCC,Saga缺少预留动作,导致某些业务的补偿动作的实现比较麻烦,比如业务是发送邮件,在TCC模式下,先保存草稿(Try)再发送(Confirm),撤销的话直接删除草稿(Cancel)就行了。而Saga则就直接发送邮件了(Ti),如果要撤销则得再发送一份邮件说明撤销(Ci)。当然,对于另外一些简单业务来说,Saga没有预留动作也可以认为是优点(详见参考博客18):

有些业务很简单,套用TCC需要修改原来的业务逻辑,而Saga只需要添加一个补偿动作就行了。

TCC最少通信次数为2n,而Saga为n(n=sub-transaction的数量)。

有些第三方服务没有Try接口,TCC模式实现起来就比较tricky了,而Saga则很简单。

没有预留动作就意味着不必担心资源释放的问题,异常处理起来也更简单(请对比Saga的恢复策略和TCC的异常处理)。

7、基于改进XA协议的阿里GTS

GTS,最初名为TXC,是Taobao Transaction Constructor的缩写,于2014年4月立项,2014年10月发布了TXC1.0版本,2015年12月发布TXC 2.0版本,2017年2月阿里云公测,外部更名为GTS(GlobalTransaction Service)。2019年1月,阿里分布式事务框架GTS开源了一个免费社区版Fescar。

前面在介绍XA规范的时候提到,两阶段提交过程资源都是处于锁定状态,为了便于对比,我再次贴出XA规范的提交示意图(参考博客21):

由图可知,无论Phase2的决议是commit还是rollback,事务性资源的锁都要保持到Phase2完成才释放。设想一个正常运行的业务,大概率是90%以上的事务最终应该是成功提交的,我们是否可以在Phase1 就将本地事务提交呢?这样 90% 以上的情况下,可以省去 Phase2 持锁的时间,整体提高效率。

 

分支事务中数据的 本地锁 由本地事务管理,在分支事务 Phase1 结束时释放。

同时,随着本地事务结束,连接 也得以释放。

分支事务中数据的 全局锁(详见参考博客25) 在事务协调器侧管理,在决议 Phase2 全局提交时,全局锁马上可以释放。只有在决议全局回滚的情况下,全局锁 才被持有至分支的 Phase2 结束。

这个设计,极大地减少了分支事务对资源(数据和连接)的锁定时间,给整体并发和吞吐的提升提供了基础。

当然,你肯定会问:Phase1 即提交的情况下,Phase2 如何回滚呢?首先,应用需要使用 Fescar的 JDBC 数据源代理,也就是 Fescar 的 RM。

Phase1:

Fescar 的 JDBC 数据源代理通过对业务 SQL 的解析,把业务数据在更新前后的数据镜像组织成回滚日志,利用本地事务的ACID 特性,将业务数据的更新和回滚日志的写入在同一个本地事务中提交。

这样,可以保证:任何提交的业务数据的更新一定有相应的回滚日志存在。

基于这样的机制,分支的本地事务便可以在全局事务的 Phase1 提交,马上释放本地事务锁定的资源。

Phase2:

如果决议是全局提交,此时分支事务此时已经完成提交,不需要同步协调处理(只需要异步清理回滚日志),Phase2 可以非常快速地完成。

如果决议是全局回滚,RM 收到协调器发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新SQL 并执行,以完成分支的回滚。

 

由此可知,Fescar的核心原理是,对业务SQL进行解析,把业务数据在更新前后的数据镜像组织成回滚日志,利用本地事务的ACID特性,将业务数据的更新和回滚日志的写入在同一个本地事务中提交。除此之外,为了保证分布式事务的隔离性,在事务协调器侧还增加了一把全局锁,以保证回滚日志得以顺利执行(可以回过头再看看Saga方案中列举的回滚失败的示例)。

小结:以ACID来衡量Fescar可知,该方案能保证AID特性,以及准实时的最终一致性。实际上,对于分布式系统,如果分布式方案能同时保证AID特性以和准实时的最终一致性,就等价于能保证增删改操作的ACID特性,而至于查询操作,可能会读取到事务中间状态的数据,这在绝大多数业务场景中都是能接受的。而且前面也讲了,对于像银行业务等对一致性有极致要求的极端场景,也可以通过在业务系统中使用分布式锁或者分布式队列来保证。

此外,对比TCC和Fescar可知,TCC无论事务最终是提交还是回滚,本质上都需要对同一个资源执行两次操作,一次是try,另一次是confirm或者cancel;而对于Fescar来说,大多数情况下,分布式事务都不需要回滚,而对于不需要回滚的分布式事务,每个资源只需要执行一次操作。从这个角度来说,Fescar的平均性能将比TCC更高。

总结

基于可靠消息中间件的分布式方案的核心原理是对发送到消息中间件的消息进行“两阶段提交”,即先提交,执行完本地事务后再确认消息。通过这样的机制,给事务的回滚提供了可能。该方案能保证事务的最终原子性和持久性,但无法保证一致性和隔离性。基于可靠消息服务的分布式方案适用于对业务的实时一致性以及事务的隔离性要求都不高的系统。

最大努力尝试方案的核心原理是依靠额外的校对系统或者报警系统来保证分布式事务。该方案能保证事务的最终原子性和持久性,但无法保证一致性和隔离性。基于可靠消息服务的分布式方案适用于对业务的实时一致性以及事务的隔离性要求都不高的系统。

TX-LCN方案的核心原理是通过协调本地事务来实现分布式事务,分布式事务的实现依赖于本地事务。一般来说,如果本地事务都能保证ACID,那么基于LCN的分布式事务也能满足AID,而不能满足一致性。TX-LCN实现相对简单,但事务对资源的锁定时间长,因而适用于对并发性能要求不高的场景。

X/Open DTP的核心原理是基于两阶段提交(XA规范),通过在整个提交过程中对资源进行锁定来实现分布式事务,能很好地满足AID特性,以及准实时的最终一致性。由于该方案对资源的锁定时间长,因而适用于对并发性能要求不高的场景。

TCC的核心原理是,在分布式事务操作中,先将所需资源预占,然后将锁释放,最后再根据资源预占的情况来决定使用资源还是退回资源。相比于XA规范,TCC方案采用预留资源的方式,将两阶段提交过程中的全局锁分成了两段本地事务锁,缩短了分布式资源锁定的时间,从而提高了事务的并发度。

saga模式的核心原理是,将一个全局事务分成若干个能独立提交的本地事务,每个本地事务都对应一个反向补偿操作,当本地事务提交失败后,通过反向补偿操作来取消本地事务的影响。相比于TCC,Saga缺少预留动作,导致某些业务的补偿动作的实现比较麻烦,比如业务是发送邮件,但对于另外一些简单业务来说,Saga没有预留动作的特性降低了老系统接入Saga方案的成本。

Fescar的核心原理是,对业务SQL进行解析,把业务数据在更新前后的数据镜像组织成回滚日志,利用本地事务的ACID特性,将业务数据的更新和回滚日志的写入在同一个本地事务中提交。此外,为了保证分布式事务的隔离性,在事务协调器侧还增加了一把全局锁,以保证回滚日志得以顺利执行。

如果觉得本文对你有帮助,可以点赞关注支持一下,也可以关注我公众号,上面有更多技术干货文章以及相关资料共享,大家一起学习进步!