分布式系统,与传统单体架构系统相比,其结构复杂的多,系统间靠网络传输数据,系统存在诸多不确定因素,如硬件故障、网络波动等,都会影响整个系统的稳定性。而分布式事务更是分布式系统的一大难题,本篇将讨论业界分布式事务的几种常见解决方案。

1. 数据库事务

百度百科对事务的定义:

数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。

解读一下这个定义:

  • 事务产生于一个数据库操作序列:一组读取或者更新数据库等操作时,事务是一种机制,来保证了最终数据的一致性,

  • 事务是一个不可分割的单元:一组操作不能拆分,是一个整体

  • 事务执行:要么全部执行,要么全部不执行,不能部分执行

通常,数据库的事务是通过数据库日志来保证的,所有数据库的操作都记录日志,如果数据库发生宕机,那么通过读取日志可以知道操作时需要回滚还是提交。

众所周知,数据库事务有ACID四大特性,满足ACID特性的事务,一般称之为刚性事务,常见的单体应用(单个数据源)内部都是采用刚性事务。

最常见的事务的例子就是银行转账:

d4a99683a3c048a4819d384f3e312c96

2. 分布式事务

分布式事务,指的是分布式系统间完成的事务机制,百度百科定义如下:

数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。

分布式同数据库事务一样,都用以保证数据的一致性,只是事务的参与者由原来的单数据源的一组操作序列变成了分布式系统的多个子系统。

举个例子:

用户下单流程,如下图所示:

e876640cac53406b92ae16704be7b8fb

首先,订单系统订单支付完成,然后调用库存系统扣减库存,最后再调用积分系统给账户添加积分,第1、2、3步必须全部执行成功,那么下单操作才算成功,否则,有一步出现错误,那么下单操作就是失败的。与传统事务相比,分布式事务范围在数据库事务之上进行了放大,各个系统除了有本系统事务外,还需要符合整个分布式系统的事务机制,才能保证最终的数据一致性。

但是,在分布式系统中,由于其固有的复杂性,很难保证事务的ACID特性,这种刚性事务在分布式系统中不适用,这就要提到两个概念:CAP定理和BASE理论。

2.1. CAP理论

2000年,名为Eric Brewer的教授在PODC研讨会上提出了CAP:一致性、可用性和分区容错性三者无法在分布式系统中被同时满足,并且最多只能满足其中两个!他还对CAP进行明确的定义:

  • C(Consistency,一致性):所有的节点上的数据时刻保持同步

  • A(Avallable,可用性):每个请求都能接受到一个响应,无论响应成功或失败

  • P(Partition tolerance,分区容错性):系统应该能持续提供服务,即使系统内部有消息丢失(分区)

CAP理论的提出,揭露了分布式系统的本质。

怎么理解CAP?

一致性,其实就是分布式系统各节点间数据达成一致;可用性,分布式系统始终保持可用,即使出现了数据不一致;分区容错性,分布式系统必须有一定的容错能力,当系统数据丢失或者某些节点不可用(分区)系统还能继续提供服务,系统容错是不可缺少的。

既然CAP三者不能同时满足,而分布式系统中,分区容错性是不可或缺的,因此,只能在CP和AP中间选择。

由于在CAP理论要求各节点数据时刻保持同步,但是这样的强一致性只能在CP中保证,而选择满足AP时无法保证,这就引出BASE理论。

2.2. BASE理论

eBay的架构师Dan Pritchett源于对大规模分布式系统的实践总结,在ACM上发表文章提出BASE理论,BASE理论是对CAP理论的延伸,核心思想是即使无法做到强一致性,但应用可以采用适合的方式达到最终一致性(Eventual Consitency)

  • BA: Basically Available(基本可用)

  • S: Soft state(软状态)

  • E: Eventually consistent(最终一致性)

显然,BASE理论满足CAP中的AP,从而舍弃了强一致性C。

满足BASE理论,放弃强一致性,数据最终达到一致的事务我们称之为柔性事务(Flexible Transactions)。

如果分布式系统满足AP,那么虽然数据不可能时刻保持一致,但是可以达到最终一致;如果满足产CP,那么分布式系统舍弃了高可用性,却可以保持数据强一致。分布式系统中,常见的两个服务注册中心Eureka、Consul,前者满足的是AP,而后者满足的是CP。

3. 分布式事务的解决方案

分布式事务解决的核心问题就是分布式系统中各个节点的数据一致性问题,选择强一致还是最终一致,需要根据具体业务需要合理做出选择。目前,常见的几种分布式事务解决方案有2PC(两阶段提交)、3PC(三阶段提交)、TCC(补偿性事务)、本地消息表等。

3.1. 2PC

两阶段提交(2PC),其实是XA协议的标准实现,XA协议由Tuxedo首先提出的,并交给X/Open组织,作为资源管理器(数据库)与事务管理器的接口标准。目前,Oracle、Informix、DB2和Sybase等各大数据库厂家都提供对XA的支持。

2PC事务包括几个角色:事务协调者(coordinator)、事务参与者(participant  )、资源管理器(resource manager,RM):

  • 事务协调者:负责收集事务参与者的信息,并决定事务提交/回滚操作

  • 事务参与者:按照事务协调者要求参与事务,可进行事务预提交、提交或者回滚操作

  • 资源管理器:事务参与者管理的数据库

2PC将事务的管理分为两个阶段:

1、第一阶段(prepare):事务协调者要求每个事务参与者预提交(precommit)事务,事务并不会真正提交,并反馈(vote)提交结果

2、第二阶段(commit/rollback):事务协调者收集参与者信息,如果每个参与者都反馈可提交事务,那么协调者下发commit指令给参与者要求提交事务,否则,如果有参与者反馈回滚事务,则协调者下发abort指令要求参与者都回滚事务

09c31fc9680d4926bd40a2ec66050506

通俗的讲:协调者首先发起事务,说参与者们检查一下看看各自操作是否能够正常执行啊,不管可以与否都给我一个反馈。然后,参与者检查完成,都反馈说,老大,我们都能正常执行,那么协调者就说好吧,你们都提交事务吧;如果某一个参与者说,完了,我操作执行失败了,不能提交事务,那么协调者就告诉各参与者:某个哥们儿不能提交事务,数据一致性没法保证了,你们都回滚吧!

2PC虽然极大的保证了数据一致性,但是存在很多缺陷:

首先,事务协调者很重要,如果它挂掉了,整个事务就瘫痪了,所有参与者都有可能阻塞,这就是单点问题;

其次,性能太差,第一阶段参与者反馈了信息,就需要阻塞等待协调者的第二个指令下达,期间啥也不干。

3.2. 3PC

由于2PC存在单点故障、阻塞等问题,后来有人又提出了无阻塞的三阶段提交协议(3PC)来扩展2PC。

6eb02f1352ef4e1e826455ce088948c9

3PC将2PC的第一阶段拆分为两个阶段:

1、决定阶段:协调者向所有参与者发出准备消息(prepare)(同2PC),若任一参与者回答中止Abort消息,则进入第三段(执行段),协调者发出Rollback命令;若所有参与者都回答Vote-Commit消息,则进入第二段(准备提交段)。

2、准备提交阶段:由协调者发准备提交消息(Prepare to Commit),参与者收到该消息后写入日志记录(Log record)中,并回答确认消息ACK

3、执行阶段:协调者根据参与者回答的ACK/Abort消息,向参与者发Commit/Rollback命令,参与者根据协调者的命令决定执行提交或回滚,完成事务的处理。这一阶段同2PC的第二阶段。

3PC是如何解决阻塞问题的?

  • 参与者宕机:如果决定阶段协调者未收到参与者的vote反馈,直接终止事务;准备提交阶段,收到协调者的prepare to commit消息后,参与者会写入日志记录,表明收到该消息,另外,所有参与者间可以进行通信。如果协调者发生故障,参与者间相互通信,来决定是否提交事务:如果参与者都收到了prepare to commit消息,则强制提交事务,如果有参与者没有收到则回滚事务。

  • 协调者宕机:协调者发出prepare消息后宕机,由于并未进入第二阶段,参与者并未开始处理事务;如果后两个阶段宕机,则由参与者依据日志和收到prepare to commit的参与者数量决定是否强制提交事务或回滚。

虽然3PC解决了2PC的阻塞问题,但是增加了通信次数,实现复杂度也变得很高。

3.3. TCC

TCC事务是一种补偿性事务机制,事务参与者的每个操作都需要提供预处理(try)、确定(confirm)、补偿(cancel)三个操作,与2PC类似。其核心思想是:先将数据更改为一个中间状态(预处理),预处理成功与否都想事务协调者反馈消息,然后事务协调者根据参与者的信息来下达事务commit或rollback指令,参与者根据协调者指令在调用自身的提交/补偿操作。

TCC分为三个阶段:

  • try: 对数据进行预处理,而不是直接提交事务

  • confirm: 将预处理的数据更新为正确的数据,相当于提交事务

  • cancel: 将预处理的数据还原到之前的状态,相当于回滚事务

e876640cac53406b92ae16704be7b8fb

还是以上边的下单过程为例,我们来分析TCC的处理过程,我们先假设扣减库存为1,添加积分为10。

3.3.1. try阶段

在该阶段,所有的操作都是做预处理,并不会执行真正的业务逻辑操作。

首先,订单支付完成后,先不将订单修改为已支付的状态,而是改为一个中间状态,假设为"UPDATING";

其次,库存系统也不立即扣减库存,库存表设计一个中间字段frozen_stock,来表示冻结库存,然后将该字段值设置为1,表明有1个库存数量待处理;

然后,积分系统的积分表同样设计一个frozen_credit的字段,表示冻结积分,然后将其值设置10.

可以看到,参与事务的三方都做了预处理操作,与本地事务没有关系,你该提交还是提交你的。

3.3.2. confirm阶段

在该阶段,需要用到TCC事务框架来做后续事务管理,有能力的话也可以自行实现,常见的国内开源的TCC事务管理框架如ByteTCC、tcc-tranaction等。TCC事务框架的作用在于,接管参与者本地事务,感知其在try阶段的本地事务执行结果,然后判断全局事务的流向(是提交还是回滚)。

接上边的例子,参与者三方本地事务状态会被TCC框架感知到。

假设三个参与者本地预处理事务都执行都成功,那么TCC框架会发起全局事务提交流程,调用参与者的提供的确认操作,即:

  • 订单系统修改订单状态为已支付

  • 库存系统将原库存更新为(原库存 - frozen_stock)的值,并重置frozen_stock为0;

  • 积分系统将原积分更新为(原积分 + frozen_credit)的值,并重置frozen_credit为0;

3.3.3. cancel阶段

如果有参与者本地事务提交失败了,则发起全局事务回滚流程,进入cancel阶段,调用参与者提供的cancel操作,即:

  • 订单系统修改订单状态为已取消;

  • 库存系统重置frozen_stock为0,而不需要修改原库存;

  • 积分系统重置frozen_credit为0,而不需要修改原积分;

TCC事务框架远没有想的那么简单。除了能够感知参与者事务状态,TCC事务框架还要记录分布式事务活动日志,以保证系统系统恢复后能够按照日志进行数据恢复。比如,上边的订单系统在事务推动过程中挂了,TCC框架要根据已经记录的日志,在其回复过后推动其恢复数据,以保证一致性。另外,如果在confirm阶段或者cancel阶段一直执行失败,TCC事务框架还要负责不停地重复调用它们,务必保证它们执行成功。

TCC事务原理简单,实现起来也很方便,一般多为公司内部研发TCC事务框架,也可以采用开源的,但是每个操作都要提供三种方法,代码的侵入性比较大,开发成本较高。

另外,TCC事务在处理分布式系统间同步方法调用时比较简单,但是如果系统间使用MQ消息来进行异步调用时,TCC事务处理的难度较高。

3.4. 本地消息表

其核心思想是将本地操作拆分为两个步骤:业务处理 + 消息存储,这两个步骤必须放在同一个事务中,保证其原子性。该方案来自eBay,其本质是将分布式事务转化为本地事务。

453e8b2481934c6a954033a43d73f6e8

事务发起方、事务消费方在本地都有一个消息表,用来持久化业务处理消息,并且会有单独的线程来扫描本地消息表,并发送到MQ。这个方案的大概流程如下:

首先,事务发起方将写业务数据和写本地消息数据放在同一个事务,保证业务数据和消息数据同时持久化;业务操作完成后存储一条处理消息,状态为未投递

其次,事务发起方后台有线程轮询本地消息表,扫描未投递的消息,然后尝试投递到MQ,并修改状态为已投递

然后,事务消费方接收MQ消息后,将其存储到本地消息表,然后进行业务处理。如果业务处理失败,则拷贝之前接收到的消息然后标记为回滚状态,然后本地线程扫描它们并发送到MQ以告知业务发起方需要进行补偿操作;

最后,事务发起方接收到消费方发来的业务补偿消息,进行业务补偿处理。

该方法满足BASE理论的最终一致性,实际实现过程中,还涉及到异常重试机制、消息可靠性等复杂逻辑,同时此方案的变化也比较多。这里仅讨论一下大概的流程,后边会单独发文来讨论该方案的诸多细节。虽然这个方案业务耦合性比较高,但是由于其原理简单,实现复杂度也不算高,还是被大多公司采用。

4. 总结

本文讨论了分布式事务的几种常用解决方案,采用哪种方案需要根据自身业务进行选择。除了上述方案外,还有一些开源的方案,如阿里巴巴开源的Seata、阿里云全局事务服务GTS、基于RocketMQ事务消息等。


相关阅读