分布式事务理论

一个数据库事务通常包含了一个序列的对数据库的读/写操作。它的存在包含有以下两个目的:

  • 为数据库操作序列提供了一个从失败中恢复到正常状态的方法,同时提供了数据库即使在异常状态下仍能保持一致性的方法。
  • 当多个应用程序在并发访问数据库时,可以在这些应用程序之间提供一个隔离方法,以防止彼此的操作互相干扰。

并非任意的对数据库的操作序列都是数据库事务。数据库事务拥有以下四个特性,习惯上被称之为 ACID特性。

  • 原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
  • 一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束。
  • 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
  • 持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库中。

对于分布式系统而言,要保证分布式系统中的数据一致性就需要一种方案,可以保证数据在子系统中始终保持一致,避免业务出现问题。这种实现方案就叫做分布式事务,要么一起成功,要么一起失败,必须是一个整体性的事务。

分布式中数据设计需要遵循的理论基础:CAP 理论和 BASE 理论。

CAP 理论

1
2
3
4
CAP,Consistency Availability Partition tolerance 的简写:
Consistency:一致性,对某个客户端来说,读操作能够返回最新的写操作结果。
Availability:可用性,非故障节点在合理的时间内返回合理的响应。
Partition tolerance:分区容错性,当出现网络分区后,系统能够继续提供服务,你知道什么是网络分区吗?

因为分布式系统中系统肯定部署在多台机器上,无法保证网络做到 100% 的可靠,所以 P 一定存在。

在出现网络分区后,必须要在这两者之间进行取舍,因此就有了两种架构:CP 架构 和 AP 架构

  • CP架构:违背了可用性的要求,只满足一致性和分区容错,即 CP,CAP 理论是忽略网络延迟,从系统 A 同步数据到系统 B 的网络延迟是忽略的。CP 架构保证了客户端在获取数据时一定是最近的写操作,或者获取到异常信息,绝不会出现数据不一致的情况。
  • AP 架构:违背了一致性的要求,只满足可用性和分区容错,即 AP,AP 架构保证了客户端在获取数据时无论返回的是最新值还是旧值,系统一定是可用的。CAP 理论关注粒度是数据,而不是整体系统设计的策略

BASE 理论

BASE 理论指的是基本可用 Basically Available,软状态 Soft State,最终一致性 Eventual Consistency,核心思想是即便无法做到强一致性,但应该采用适合的方式保证最终一致性。

BASE,Basically Available Soft State Eventual Consistency 的简写:

1
2
3
BA:Basically Available 基本可用,分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。
S:Soft State 软状态,允许系统存在中间状态,而该中间状态不会影响系统整体可用性。
E:Consistency 最终一致性,系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。

BASE 理论本质上是对 CAP 理论的延伸,是对 CAP 中 AP 方案的一个补充。

分布式事务协议

目前较为流行的分布式事务解决方案可以分为几种:

两阶段提交 X/Open XA 协议

XA 是一个分布式事务协议,由 Tuxedo 提出。XA 规范主要定义了(全局)事务管理器(Transaction Manager)和(局部)资源管理器(Resource Manager)之间的接口。XA 接口是双向的系统接口,在事务管理器(Transaction Manager)以及一个或多个资源管理器(Resource Manager)之间形成通信桥梁。

20200423150038

XA 协议采用两阶段提交方式来管理分布式事务。XA 接口提供资源管理器与事务管理器之间进行通信的标准接口。

1
XA协议比较简单,而且一旦商业数据库实现了XA协议,使用分布式事务的成本也比较低。但是,XA也有致命的缺点,那就是性能不理想,特别是在交易下单链路,往往并发量很高,XA无法满足高并发场景。XA目前在商业数据库支持的比较理想,在MySQL数据库中支持的不太理想,mysql的XA实现,没有记录prepare阶段日志,主备切换回导致主库与备库数据不一致。许多nosql也没有支持XA,这让XA的应用场景变得非常狭隘。

2PC:二阶段提交协议

二阶段提交(Two-phase Commit),是指,为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法(Algorithm)。通常,二阶段提交也被称为是一种协议(Protocol)。

在分布式系统中,每个节点虽然可以知晓自己的操作是成功或者失败,却无法知道其他节点的操作是成功或失败。

当一个事务跨越多个节点时,为了保持事务的 ACID 特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。

因此,二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。

1
2
3
4
5
6
7
8
投票阶段 Prepares:
协调者向所有参与者询问是否可以执行提交操作,并开始等待各参与者的响应。
参与者执行事务操作,如果执行成功就返回 Yes 响应,如果执行失败就返回 No 响应。
如果协调者接受参与者响应超时,也会认为执行事务操作失败。

提交阶段 Commit:
如果第一阶段汇总所有参与者都返回 Yes 响应,协调者向所有参与者发出提交请求,所有参与者提交事务。
如果第一阶段中有一个或者多个参与者返回 No 响应,协调者向所有参与者发出回滚请求,所有参与者进行回滚操作。

二阶段提交优点:尽量保证了数据的强一致,但不是 100% 一致。

二阶段提交缺点:

1
2
3
4
单点故障,由于协调者的重要性,一旦协调者发生故障,参与者会一直阻塞,尤其是在第二阶段,协调者发生故障,那么所有的参与者都处于锁定事务资源的状态中,而无法继续完成事务操作。
同步阻塞,由于所有节点在执行操作时都是同步阻塞的,当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
数据不一致,在第二阶段中,当协调者向参与者发送提交事务请求之后,发生了局部网络异常或者在发送提交事务请求过程中协调者发生了故障,这会导致只有一部分参与者接收到了提交事务请求。
而在这部分参与者接到提交事务请求之后就会执行提交事务操作。但是其他部分未接收到提交事务请求的参与者则无法提交事务。从而导致分布式系统中的数据不一致。

二阶段提交的问题:如果协调者在第二阶段发送提交请求之后挂掉,而唯一接受到这条消息的参与者执行之后也挂掉了,即使协调者通过选举协议产生了新的协调者并通知其他参与者进行提交或回滚操作的话,都可能会与这个已经执行的参与者执行的操作不一样。

当这个挂掉的参与者恢复之后,就会产生数据不一致的问题。

3PC:三阶段提交协议

三阶段提交(Three-phase commit),是为解决两阶段提交协议的缺点而设计的。与两阶段提交不同的是,三阶段提交是“非阻塞”协议。

三阶段提交在两阶段提交的第一阶段与第二阶段之间插入了一个准备阶段,使得原先在两阶段提交中,参与者在投票之后,由于协调者发生崩溃或错误,而导致参与者处于无法知晓是否提交或者中止的“不确定状态”所产生的可能相当长的延时的问题得以解决。

三阶段提交的三个阶段:CanCommit/PreCommit/DoCommit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
询问阶段:CanCommit
协调者向参与者发送 Commit 请求,参与者如果可以提交就返回 Yes 响应,否则返回 No 响应。

准备阶段:PreCommit
协调者根据参与者在询问阶段的响应判断是否执行事务还是中断事务:
如果所有参与者都返回 Yes,则执行事务。
如果参与者有一个或多个参与者返回 No 或者超时,则中断事务。

参与者执行完操作之后返回 ACK 响应,同时开始等待最终指令。

③提交阶段:DoCommit
协调者根据参与者在准备阶段的响应判断是否执行事务还是中断事务:
如果所有参与者都返回正确的 ACK 响应,则提交事务。
如果参与者有一个或多个参与者收到错误的 ACK 响应或者超时,则中断事务。
如果参与者无法及时接收到来自协调者的提交或者中断事务请求时,会在等待超时之后,会继续进行事务提交。

协调者收到所有参与者的 ACK 响应,完成事务。

在三阶段提交中,如果在第三阶段协调者发送提交请求之后挂掉,并且唯一的接受的参与者执行提交操作之后也挂掉了,这时协调者通过选举协议产生了新的协调者。

在二阶段提交时存在的问题就是新的协调者不确定已经执行过事务的参与者是执行的提交事务还是中断事务。

但是在三阶段提交时,肯定得到了第二阶段的再次确认,那么第二阶段必然是已经正确的执行了事务操作,只等待提交事务了。

所以新的协调者可以从第二阶段中分析出应该执行的操作,进行提交或者中断事务操作,这样即使挂掉的参与者恢复过来,数据也是一致的。

所以,三阶段提交解决了二阶段提交中存在的由于协调者和参与者同时挂掉可能导致的数据一致性问题和单点故障问题,并减少阻塞。因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行提交事务,而不会一直持有事务资源并处于阻塞状态。

三阶段提交的问题:在提交阶段如果发送的是中断事务请求,但是由于网络问题,导致部分参与者没有接到请求。那么参与者会在等待超时之后执行提交事务操作,这样这些由于网络问题导致提交事务的参与者的数据就与接受到中断事务请求的参与者存在数据不一致的问题。

强一致性分布式事务

基于 2PC/XA 协议实现的 JTA

1
2
3
Transaction Manager:常用方法,可以开启,回滚,获取事务。begin(),rollback()...
XAResouce:资源管理,通过 Session 来进行事务管理,commit(xid)...
XID : 每一个事务都分配一个特定的 XID。

JTA 主要的原理是二阶段提交,当整个业务完成了之后只是第一阶段提交,在第二阶段提交之前会检查其他所有事务是否已经提交。

如果前面出现了错误或是没有提交,那么第二阶段就不会提交,而是直接回滚,这样所有的事务都会做回滚操作。基于 JTA 这种方案实现分布式事务的强一致性。

JTA 的特点:基于两阶段提交,有可能会出现数据不一致的情况;事务时间过长,阻塞;性能低,吞吐量低

最终一致性分布式事务方案

JTA 方案适用于单体架构多数据源时实现分布式事务,但对于微服务间的分布式事务就无能为力了,我们需要使用其他的方案实现分布式事务。

本地消息表

本地消息表的核心思想是将分布式事务拆分成本地事务进行处理。

以本文中例子,在订单系统新增一条消息表,将新增订单和新增消息放到一个事务里完成,然后通过轮询的方式去查询消息表,将消息推送到 MQ,库存系统去消费 MQ。

消息事务+最终一致性

基于消息中间件的两阶段提交,本质上是对消息中间件的一种特殊利用,它是将本地事务和发消息放在了一个分布式事务里,保证要么本地操作成功成功并且对外发消息成功,要么两者都失败,开源的RocketMQ就支持这一特性。具体原理如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1、A系统向消息中间件发送一条预备消息
2、消息中间件保存预备消息并返回成功
3、A执行本地事务
4、A发送提交消息给消息中间件

通过以上4步完成了一个消息事务。对于以上的4个步骤,每个步骤都可能产生错误:

步骤一出错,则整个事务失败,不会执行A的本地操作
步骤二出错,则整个事务失败,不会执行A的本地操作
步骤三出错,这时候需要回滚预备消息,怎么回滚?答案是A系统实现一个消息中间件的回调接口,消息中间件会去不断执行回调接口,检查A事务执行是否执行成功,如果失败则回滚预备消息
步骤四出错,这时候A的本地事务是成功的,那么消息中间件要回滚A吗?答案是不需要,其实通过回调接口,消息中间件能够检查到A执行成功了,这时候其实不需要A发提交消息了,消息中间件可以自己对消息进行提交,从而完成整个消息事务
基于消息中间件的两阶段提交往往用在高并发场景下,将一个分布式事务拆成一个消息事务(A系统的本地操作+发消息)+B系统的本地操作,其中B系统的操作由消息驱动,只要消息事务成功,那么A操作一定成功,消息也一定发出来了,这时候B会收到消息去执行本地操作,如果本地操作失败,消息会重投,直到B操作成功,这样就变相地实现了A与B的分布式事务。

此方案能够完成A和B的操作,但是A和B并不是严格一致的,而是最终一致的,我们在这里牺牲了一致性,换来了性能的大幅度提升。当然,这种玩法也是有风险的,如果B一直执行不成功,那么一致性会被破坏,具体要不要玩,还是得看业务能够承担多少风险。

TCC

TCC(Try、Confirm、Cancel)是两阶段提交的一个变种。TCC提供了一个框架,需要应用程序按照该框架编程,将业务逻辑的每个分支都分为Try、Confirm、Cancel三个操作集。TCC让应用程序自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量成为可能。

以一个典型的淘宝订单为例,按照TCC框架,应用需要在Try阶段将商品的库存减去,将买家支付宝账户中的相应金额扣掉,在临时表中记录下商品的数量,订单的金额等信息;另外再编写Confirm的逻辑,即在临时表中删除相关记录,生成订单,告知CRM、物流等系统,等等;以及Cancel逻辑,即恢复库存和买家账户金额,删除临时表相关记录。

TCC就是通过代码人为实现了两阶段提交,不同的业务场景所写的代码都不一样,复杂度也不一样,因此,这种模式并不能很好地被复用。