请回答事务(2)-事务分类和2PC

217 阅读29分钟

一、事务分类

分布式事务是解决分布式场景下的分布式数据一致性问题的。

在分布式事务进行的过程中,一致性是无法得到保证的,但是分布式事务完成之后,一致性是严格遵守的。因此我们将分布式事务方案称为最终一致性方案,这个最终一致性,与CAP中的最终一致性用了同样的词语,但他们的具体含义是不一样的,在CAP中是指读取操作最终能够读取到最后一次写入的结果,在分布式事务中是指最终事务完成后,数据严格满足业务约束。

实现方案从类型上可以分为刚性事务和柔性事务。

  • 刚性事务:强一致性,原生支持回滚/隔离性,通常无业务改造,低并发,适合短事务。如XA协议(2PC、JTA、JTS),3PC

  • 柔性事务:有业务改造,最终一致性,实现补偿接口,实现资源锁定接口,高并发,适合长事务。如 TCC、Saga、本地事务消息、消息事务(半消息)

柔性事务又可分为消息通知和事务补偿,如TCC、Saga属于事务补偿;本地事务消息和消息事务属于消息通知。

这里要注意,强一致性的刚性事务也是最终一致性的方案,只是它可以做到外部无论何时去访问数据时,都能满足业务的约束(如银行转账场景,无论何时查询,两个账户的总额不变)。而柔性事务下,在某个时间点去访问事务内涉及到的多个资源,是有可能会有数据不一致的情况出现的。

image.png

我们看到,最终一致性解决方案有很多,它们之间的区别是什么呢?按照最终一致性的强弱:

XA > TCC > 消息 > Saga

分类如下:

  • 不一致窗口短:XA和TCC在理想的情况下,可以做到不一致的窗口时间很短

  • 不一致窗口长:SAGA和MSG则缺少控制不一致窗口时间的方法,相对来说会更长

  • XA:XA虽然不是强一致,但是XA的一致性是多种分布式事务中,一致性最好的,因为他处于不一致的状态时间很短,只有一部分分支开始commit,但还没有全部commit的这个时间窗口,数据是不一致的。因为数据库的commit操作耗时,通常是10ms内,因此不一致的窗口期很短。

  • TCC:理论上,TCC可以用XA来实现,例如Try-Prepare,Confirm-Commit,Cancel-Rollback。但绝大多数时候,TCC会在业务层自己实现Try|Confirm|Cancel,因此Confirm操作耗时,通常高于XA中的Commit,不一致的窗口时间比XA长

  • MSG:二阶消息型事务在第一个操作完成后,在所有操作完成之前,这个时间窗口是不一致的,持续时长一般比前两者更久。

  • SAGA:SAGA的不一致窗口时长与消息接近,但是如果发生回滚,而子事务中正向操作修改的数据又会被用户看到,这部分数据就是错误数据,容易给用户带来较差的体验,因此一致性是最差的。

从实现方式上来分,主要有基于补偿的方案和基于消息通知方案两种类型,基于补偿的方案在实际应用中使用的比较广泛。各个方案均有优缺点,需要按照业务场景及团队的能力水平选择一种合适的方案。 本文介绍的2PC、TCC模式、Saga模式、Seata AT模式都可以看成是遵守XA协议或是XA协议的变种。而基于消息通知的事务方案是互联网公司在高并发场景中探索出的一种应用模式,它不遵守 XA 协议。

在正式引出分布式事务模型之前,我们先从两阶段提交说起(2PC=Two Phase Commit),它是一个最原始也最简单的方案,我们从这个方案中来看一下要实现一个分布式事务,要解决哪些问题。

参考:MIT 公开课 ****zhuanlan.zhihu.com/c_127371860…

二、分布式事务原理初探

分布式事务的关键两点:

  • 并发控制:我理解为如何在并发情况下,保证结果的正确性,其实对应的是 隔离性

  • 原子提交:一个事务中的所有操作需要保证是原子的,对应的是 原子性

现在,描述一个银行转账的场景:

当前有x和y两个账户,账户都是10元,现在想从x转账到y

假设有两个事务,描述如下:

事务T1:
begin
update t_balance set count = count - 1 where id = x
update t_balance set count = count + 1 where id = y
commit

事务T2:
begin
select * from t_balance where id = x
select * from t_balance where id = y
commit

一致性保证:

事务执行前后,数据从一个一致性状态变换到另一个一致性状态。这里的一致性与 CAP 中的 C 不是一个概念。这种一致性状态是一种合法性状态,或者说是正确的状态,这种状态是需要根据具体业务确定的。

2.1 事务的可序列化

可序列化的定义

在该场景下,合法性状态就是,转账前后 x + y 的总和保持不变。

通常来说,隔离性(Isolated)意味着可序列化(Serializable)。它的定义是如果在同一时间并行的执行一系列的事务,那么可以生成一系列的结果。这里的结果包括两个方面:读和写。这其中考虑并发的话,就会有 读-读、读-写、写-读、写写 的多种场景。

我们说可序列化是指,并行的执行一些事务得到的结果,与按照某种串行的顺序来执行这些事务,可以得到相同的结果(表明我们利用了某些机制,使得事务可以得到串行执行的结果,比如加锁)。实际的执行过程或许会有大量的并行处理,但是这里要求得到的结果与按照某种顺序一次一个事务的串行执行结果是一样的。所以,如果你要检查一个并发事务执行是否是可序列化的,你查看结果,并看看是否可以找到对于同一些事务,存在一次只执行一个事务的顺序,按照这个顺序执行可以生成相同的结果。

对于上面的T1、T2,只有两种串行顺序:T1、T2 或 T2、T1。换句话说,T2能查到的值要么是 11、9 要么是 10、10。这两种结果之外的结果都是不合法的,因此我们对并发的控制至少要满足这个条件:不能看到除此之外的其它的结果

假如没有任何的机制或限制,其我们很容易想到,如果任由 T1 和 T2 并发执行,是会看到很多其它的结果的。比如:

  • T1执行完第一句,然后执行T2,此时查出来的值是 9、10
  • T2执行完第一句,然后执行T1,此时查出来的值是 10、11

此时这类事务就不是可序列化的

优点

  • 可序列化是一个应用广泛且实用的定义,背后的原因是,它定义了事务执行过程的正确性。它是一个对于程序员来说是非常简单的编程模型,作为程序员你可以写非常复杂的事务而不用担心系统同时在运行什么,或许有许多其他的事务想要在相同的时间读写相同的数据,或许会发生错误,这些你都不需要关心。可序列化特性确保你可以安全的写你的事务,就像没有其他事情发生一样。因为系统最终的结果必须表现的就像,你的事务在这种一次一个的顺序中是独占运行的。这是一个非常简单,非常好的编程模型。

  • 可序列化的另一方面优势是,只要事务不使用相同的数据,它可以允许真正的并行执行事务。我们之前的例子之所以有问题,是因为T1和T2都读取了数据X和Y。但是如果它们使用完全没有交集的数据库记录,那么这两个事务可以完全并行的执行。在一个分片的系统中,不同的数据在不同的机器上,你可以获得真正的并行速度提升,因为可能一个事务只会在第一个机器的第一个分片上执行,而另一个事务并行的在第二个机器上执行。所以,这里有可能可以获得更高的并发性能。

1.2 事务需要回滚机制

在我详细介绍可序列化的事务之前,我还想提出一个小点。有一件场景我们需要能够应付,事务可能会因为这样或那样的原因在执行的过程中失败或者决定失败,通常这被称为Abort。对于大部分的事务系统,我们需要能够处理,例如当一个事务尝试访问一个不存在的记录,或者除以0,又或者是,某些事务的实现中使用了锁,一些事务触发了死锁,而解除死锁的唯一方式就是干掉一个或者多个参与死锁的事务,类似这样的场景。所以在事务执行的过程中,如果事务突然决定不能继续执行,这时事务可能已经修改了部分数据库记录,我们需要能够回退这些事务,并撤回任何已经做了的修改。

1.3 小结

实现事务的策略,主要是两块。

  • 并发控制(Concurrency Control)。这是我们用来提供可序列化的主要工具。所以并发控制就是可序列化的别名。通过与其他尝试使用相同数据的并发事务进行隔离,可以实现可序列化。
  • 原子提交(Atomic Commit)。它帮助我们处理类似这样的可能场景:前面例子中的事务T1在执行过程中可能已经修改了X的值,突然事务涉及的一台服务器出现错误了,我们需要能从这种场景恢复。所以,哪怕事务涉及的机器只有部分还在运行,我们需要具备能够从部分故障中恢复的能力。这里我们使用的工具就是原子提交。我们后面会介绍。

三、并发控制

并发控制主要有两种策略,悲观并发控制和乐观并发控制。

悲观并发控制通常会用到锁,只有获取到了数据的锁,才能操作该数据。如果有锁冲突,就会造成未抢占到锁的事务延时等待。

乐观并发控制的基本思想是:不用担心事务之间的并发读写,对某个事务来说,只需要继续执行读写操作,然后在事务最后的时候,再检查是不是有一些其它的事务可能已经操作了数据(比如通过版本号、状态机的前置状态判断)。如果没有,则事务完成,且整个过程中不会有锁带来的性能损耗。如果有,并且造成了冲突,那么需要回滚当前事务,并且重试。

3.1 悲观并发控制

悲观并发控制中的锁,是两阶段锁。当事务需要使用一些数据记录时,对于两阶段锁来说,需要满足两个规则:

  1. 第一个规则是在使用任何数据之前或在执行任何数据的读写之前,先获取锁。

  2. 第二个对于事务的规则是,事务必须持有任何已经获得的锁,直到事务提交或者Abort,你不允许在事务的中间过程释放锁。你必须要持有所有的锁,并不断的累积你持有的锁,直到你的事务完成了。所以,这里的规则是,持有锁直到事务结束。

这也即对应了两阶段的两个阶段:首先获取锁,然后在事务结束前一直持有锁。

回到最初的例子,事务T1和T2,操作的数据都是 x 和 y,x 和 y 各有一把锁,事务在操作 x 或 y 的数据之前,需要先获取它们的锁,否则需要一直等待,直到上一个持有锁的事务完成(只有事务完成才会释放锁)。

这里基本上是迫使事务串行执行,在刚刚的例子中,两阶段锁迫使执行顺序是T2,T1或T1,T2。所以这里显式的迫使事务的执行遵循可序列化的定义,以我们可以获得正确的执行结果。

为什么一定要持有锁直到事务结束?

如果在使用数据之后立刻释放锁,而不是等到事务结束后释放,会有问题。

例1例2
T1T2T1T2
Lock(x)Lock(x)
get(x)读到x=10add(x)
unLock(x)unLock(x)
Lock(x)Lock(x)
add(x)get(x)读到x=9 9只是个临时值,实际该值并没有持久化到数据库中
unLock(x)unLock(x)
Lock(y)Lock(y)
add(y)add(y) fail
unLock(y)Rollback 此时 x = 10
Lock(y)Lock(y)
get(y)读到y=11get(y)
unLock(y)unLock(y)

死锁的问题

这里非常容易发生死锁。例如:

T1 先读取记录x,再读取记录y;T2 先读取记录y,再读取记录x。

每个事务都获取了第一个读取数据的锁,直到事务结束了,它们都不会释放这个锁。所以接下来,它们都会等待另一个事务持有的锁,除非数据库足够聪明,这里会永远死锁。

解决方案:

实际上,事务有各种各样的策略,包括了判断循环,超时来判断它们是不是陷入到这样一个场景中。如果是的话,数据库会Abort其中一个事务,撤回它所有的操作,并表现的像这个事务从来没有发生一样。

四、两阶段提交-2PC

分布式事务首先要考虑的就是原子性。在分布式事务场景下,主要的挑战是如何应对各种各样的故障。如机器宕机、消息丢失,同时还需考虑性能。保证原子性的解决方案是原子提交协议。

两阶段提交就是一种原子提交协议。

4.1 角色

事务协调者(Transaction Coordinator):管理事务,事务协调者以某种形式运行事务的代码,例如Put/Get/Add,它向持有了不同数据的其他计算机发送消息,其他计算机再执行事务的不同部分。在一个完整的系统中,可能会同时运行着很多事务,这些事务可能会操作同一个数据。每一个数据需要区分当前自己正在被哪个事务所使用,所以事务ID是必须的。事务协调器需要在本地记录事务的状态。

参与者(Participants):实际执行部分的事务

4.2 执行流程

3.2.1 第一阶段 投票

该阶段的主要目的在于确定数据库集群中的各个参与者是否能够正常的执行事务,具体步骤如下:

  1. 协调者向所有的参与者发送事务执行请求,并等待参与者反馈事务执行结果;

  2. 事务参与者收到请求之后,执行事务但不提交,并记录事务日志;

  3. 参与者将自己事务执行情况反馈给协调者,同时阻塞等待协调者的后续指令。

如何理解第2点中的“执行事务但不提交”?对于每一个参与者而言,执行的其实是本地事务。以mysql为例,就是在本地开启了事务,执行所有的sql,就差最后的 commit。此时在这里阻塞,并执行第3点

3.2.2 第二阶段 事务提交

在经过第一阶段协调者的询盘之后,各个参与者会回复自己事务的执行情况,这时候存在 3 种可能性:

  1. 所有的参与者都回复能够正常执行事务。

  2. 一个或多个参与者回复事务执行失败。

  3. 协调者等待超时。

对于第 1 种情况,协调者将向所有的参与者发出提交事务的通知,具体步骤如下:

  1. 协调者向各个参与者发送 commit 通知,请求提交事务;

  2. 参与者收到事务提交通知之后执行 commit 操作,然后释放占有的资源;

  3. 参与者向协调者返回事务 commit 结果信息。

对于第 2 和第 3 种情况,协调者均认为参与者无法成功执行事务,为了整个集群数据的一致性,所以要向各个参与者发送事务回滚通知,具体步骤如下:

  1. 协调者向各个参与者发送事务 rollback 通知,请求回滚事务;

  2. 参与者收到事务回滚通知之后执行 rollback 操作,然后释放占有的资源;

  3. 参与者向协调者返回事务 rollback 结果信息。

五、故障恢复

现在,我们需要在脑中设想各种可能发生的错误,并确认这里的两阶段提交协议是否仍然可以提供All-or-Noting的原子特性。如果不能的话,我们该如何调整或者扩展协议?

5.1 机器宕机重启

5.1.1 参与者宕机

需要考虑参与者是在哪个时间点宕机的。

参与者在回复事务协调者的 prepare 消息之前宕机

宕机时,参与者中关于事务的信息都还在内存中。重启后,参与者将丢失这部分信息(事务ID以及具体要使用哪个方法操作哪条数据等信息)。但协调者有可能重复发送 prepare 消息,由于此时参与者已经不可能再正确执行事务了,所以此时参与者必须回复 no,保证协调者不会发送 commit 消息。所以:

  • 如果参与者接收到了一个无法识别的prepare消息,需要回复no

Prepare 消息并不是参与者感知到事务的第一个请求,在此之前,协调者已经把需要参与者执行的方法信息给到了参与者,参与者明确知道自己已经身处某个事务中,并且知道自己需要执行哪些方法

参与者在给 prepare 消息回复 yes 后宕机

参与者回复了 yes 后,是会直接影响后续协调者对事务是否 commit 的决策的。因此我们认为参与者回复yes后,就相当于承诺不论发生什么情况自己一定可以将事务commit。

所以,即使宕机重启,参与者也必须能够兑现自己的承诺。这意味着,在故障重启的时候,B不能丢失对于事务的状态记录。解决方案:

在B回复Prepare之前,它必须确保记住当前事务的中间状态,记住所有要做的修改,记住事务持有的所有的锁,这些信息必须在磁盘上持久化存储。通常来说,这些信息以Log的形式在磁盘上存储。所以在B回复Yes给Prepare消息之前,它首先要将相应的Log写入磁盘,并在Log中记录所有有关提交事务必须的信息。做完这些事情后,B才会回复 yes。

之后,如果B在发送完Yes之后崩溃了,当它重启恢复时,通过查看自己的Log,它可以发现自己正在一个事务的中间,并且对一个事务的Prepare消息回复了Yes。Log里有Commit需要做的所有的修改,和事务持有的所有的锁。之后,当B最终收到了Commit而不是Abort,通过读取Log,B就知道如何完成它在事务中的那部分工作。

所以我们需要对协议做一个补充:

参与者在回复Yes给TC的Prepare消息之前,必须将Log写入到自己的磁盘中。这里会使得两阶段提交稍微有点慢,因为这里要持久化存储数据。

参与者在收到 commit 之后崩溃了

如果参与者在收到 commit 消息之后,在真正执行完本地事务的 commit 之前崩溃了。这种情况其实和第二种情况一样。参与者在重启之后可以通过Log获取事务的状态,再次 commit 即可。

但参与者有可能在处理完Commit之后就崩溃了。不过这样的话,B就完成了修改,并将数据持久化存储在磁盘上了。这样的话,故障重启就不需要做任何事情,因为事务已经完成了。但是我们需要考虑的是,由于参与者已经崩溃宕机,此时无法发送 commit 的结果给到协调者。协调者由于没有接收到参与者的 ACK,可能会再次发送 commit 消息。所以我们需要关心,如果B收到了同一个Commit消息两次,该怎么办?这里B可以记住事务的信息,但是这会消耗内存,所以实际上B会完全忘记已经在磁盘上持久化存储的事务的信息。对于一个它不知道事务的Commit消息,B会简单的ACK这条消息。这一点在后面的一些介绍中非常重要。

5.1.2 协调者宕机

简单分析下可能需要关注的时间点:

  • 发送 prepare 消息之前崩溃:显然不需要关注
  • 发送 prepare 消息之后崩溃:此时参与者可能已经将本地事务阻塞在了 commit,但是由于协调者崩溃了,参与者长时间接收不到 commit 消息,可主动向协调者查询事务的状态来决定是否abort 事务
  • 发送 commit 消息过程中崩溃:即有可能有部分参与者已经收到了 commit 消息,而部分参与者不可能收到 commit 消息。由于协调者发送出去的 commit 消息已经对后续的参与者产生了影响,相当于它对所有的参与者做出了一个承诺:保证所有参与者的事务都会提交。因此协调者在重启之后需要继续执行这个事务,向所有参与者发送 commit 消息。这就要求:

协调者决定要Commit而不是Abort事务,并且在发送任何Commit消息之前,它必须先将事务的信息写入到自己的Log,并存放在例如磁盘的持久化存储中,这样计算故障重启了,信息还会存在。

所以,事务协调者在收到所有对于Prepare消息的Yes/No投票后,会将结果和事务ID写入存在磁盘中的Log,之后才会开始发送Commit消息。

这也是参与者必须做好可能会接收到重复的 commit 消息的准备的一个原因

5.2 网络故障

如果消息在网络传输的时候丢失了怎么办?或许你发送了一个消息,但是消息永远也没有送达。或许你发送了一个消息,并且在等待回复,或许回复发出来了,但是之后被丢包了。这里的任何一个消息都有可能丢包,我们必须想清楚在这样的场景下该怎么办?

场景一

事务协调者发送了Prepare消息,但是并没有收到所有的Yes/No消息,事务协调者这时该怎么做呢?

  1. 协调者重新发送一轮 prepare 消息,表明自己没有收到全部的 yes/no 回复

问题:如果其中一个参与者需要关机很长时间,那这就会导致其它所有的参与者都必须处在一个事务待提交的等待状态。

  1. 协调者如果没有收到全部的 prepare 消息回复,可以直接 abort 事务。

之后协调者本身会清楚该事务的记录,然后如果关机的参与者恢复了,并且因为收到了协调者的prepare消息(消息可能因为网络而传输很慢)去主动查询协调者。协调者此时已经忘记了该事务,则可以直接回复参与者,直接 abort 该事务

场景二

参与者等待 prepare 消息超时。因为参与者自己本身一定可以确定自己没有回复 yes,所以事务一定不会是 commit 的。那么它也可以决定Abort事务。在之后的时间里,如果事务协调者上线或网络恢复,再次发送Prepare消息,B会说我不知道有关事务的任何事情并回复No。然后协调者会保证其他所有的参与者都会abort事务

参与者已经回复了 prepare 请求的 yes,此时处于等待 commit/abort 的状态下。但长时间没有收到消息,参与者可以直接 abort 事务吗?

不可以!因为参与者已经回复了 prepare 请求的 yes,意味着它对协调者做出了承诺。所以这里它必须无限期的等待 commit/abort 消息。这会使得自己陷入 Block 状态

这里的Block行为是两阶段提交里非常重要的一个特性,并且它不是一个好的属性。因为它意味着,在特定的故障中,你会很容易的陷入到一个需要等待很长时间的场景中,在等待过程中,你会一直持有锁,并阻塞其他的事务。

所以,人们总是尝试在两阶段提交中,将这个区间尽可能快的完成,这样可能造成Block的时间窗口也会尽可能的小。所以人们尽量会确保协议中这部分尽可能轻量化,甚至对于一些变种的协议,对于一些特定的场景都不用等待。

为什么这里的两阶段提交协议能构建一个A和B要么全Commit,要么全Abort的系统?其中一个原因是,决策是在一个单一的实例,也就是事务协调者完成的。A或者B不能决定Commit还是不Commit事务,A和B之间不会交互来达成一致并完成事务的Commit,相反的只有事务协调者可以做决定。事务协调者是一个单一的实例,它会通知其他的部分这是我的决定,请执行它。

另一个比较细节的问题,协调者在什么时间可以删除 Log 中有关事务的信息?

如果事务协调者成功的得到了所有参与者的ACK,那么它就知道所有的参与者知道了事务已经Commit或者Abort,所有参与者必然也完成了它们在事务中相应的工作,并且永远也不会需要知道事务相关的信息。所以当事务协调者得到了所有的ACK,它可以擦除所有有关事务的记忆。

类似的,当一个参与者收到了Commit或者Abort消息,完成了它们在事务中的相应工作,持久化存储事务结果并释放锁,那么在它发送完ACK之后,参与者也可以完全忘记相关的事务。

当然事务协调者或许不能收到ACK,这时它会假设丢包了并重发Commit消息。这时,如果一个参与者收到了一个Commit消息,但是它并不知道对应的事务,因为它在之前回复ACK之后就忘记了这个事务,那么参与者会再次回复一个ACK。因为如果参与者收到了一个自己不知道的事务的Commit消息,那么必然是因为它之前已经完成对这个事务的Commit或者Abort,然后选择忘记这个事务了。

5.3 小结

通过以上分析,我们来总结下 2PC 的优缺点

5.3.1 优点

两阶段提交协议原理简单、易于实现

5.3.2 缺点

  1. 单点问题

协调者在整个两阶段提交过程中扮演着举足轻重的作用,一旦协调者所在服务器宕机,就会影响整个数据库集群的正常运行。比如在第二阶段中,如果协调者因为故障不能正常发送事务提交或回滚通知,那么参与者们将一直处于阻塞状态,整个数据库集群将无法提供服务。

  1. 同步阻塞

两阶段提交执行过程中,所有的参与者都需要听从协调者的统一调度,期间处于阻塞状态而不能从事其他操作,这样效率极其低下。

  1. 数据不一致性

两阶段提交协议虽然是分布式数据强一致性所设计,但仍然存在数据不一致性的可能性。比如在第二阶段中,假设协调者发出了事务 commit 通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了commit 操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。虽然在网络恢复后,其余参与者最终会收到 commit 通知并提交事务而达到最终一致,但这依赖恢复时间,这也是在业务上不太能接受的。

针对上述问题,引入超时机制互询机制

超时机制: 对于协调者来说如果在指定时间内没有收到所有参与者的应答,则可以自动退出等待状态,并向所有参与者发送 rollback 通知。

互询机制: 让参与者 A 去询问其他参与者 B 的执行情况。如果 B 执行了 rollback 或 commit 操作,则 A 可以大胆的与 B 执行相同的操作;如果 B 此时还没有到达 READY 状态(未回复prepare请求yes),则可以推断出协调者发出的肯定是 rollback 通知;如果 B 同样位于 READY 状态,则 A 可以继续询问另外的参与者。只有当所有的参与者都位于 READY 状态时,此时两阶段提交协议无法处理,将陷入长时间的阻塞状态。退化为引入互询机制前状态。

六、总结

总体来说,两阶段提交的名声其实很不好。原因是:

  • 因为有多轮消息的存在,它非常的慢。
  • 有大量的写磁盘操作,比如说参与者在回复Yes给Prepare消息之后不仅要向磁盘写入数据,协调者发送commit消息前也需要写数据。理论上讲,写一次数据到磁盘中需要耗费 10ms,那么至少需要20ms,在这至少20ms的时间内,数据的锁都被参与者持有,其它使用相关数据的事务都会被阻塞。

两阶段提交在面对故障时是非常脆弱的,在故障时它可以有正确的结果,但是不具备可用性。所以,这里的问题是,是否可以构建一个合并的系统,同时具备Raft的高可用性,但同时又有两阶段提交的能力将事务分包给不同的参与者。这里的结构实际上是,通过Raft或者Paxos或者其他协议,来复制两阶段提交协议里的每一个组成部分。

使用Raft可以通过将数据复制到多个参与者得到高可用。Raft的意义在于,即使部分参与的服务器故障了或者不可达,系统仍然能工作。Raft能做到这一点是因为所有的服务器都在做相同的事情,所以我们不需要所有的服务器都参与,我们只需要过半服务器参与。然而两阶段提交,参与者完全没有在做相同的事情,每个参与者都在做事务中的不同部分,比如A可能在对X加1,B可能在对Y减1。所以在两阶段提交中,所有的参与者都在做不同的事情。所有的参与者都必须完成自己那部分工作,这样事务才能结束,所以这里需要等待所有的参与者。

是有可能结合这两种协议的,一种方式就是:

我们会有三个不同的集群,事务协调器会是一个复制的服务,包含了三个服务器,我们在这3个服务器上运行Raft,其中一个服务器会被选为Leader,它们会有复制的状态,它们有Log来帮助它们复制,我们只需要等待过半服务器响应就可以执行事务协调器的指令。事务协调器还是会执行两阶段提交里面的各个步骤,并将这些步骤记录在自己的Raft集群的Log中。

每个事务参与者也同样是一个Raft集群。最终,消息会在这些集群之间传递。不得不承认,这里很复杂,但是它展示了你可以结合两种思想来同时获得高可用和原子提交。

参考资料

最后

  • 如果觉得有收获,三连支持下;
  • 文章若有错误,欢迎评论留言指出,也欢迎转载,转载请注明出处;
  • 个人vx:Listener27, 交流技术、面试、学习资料、帮助一线互联网大厂内推等

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿