12.5 总结
最后更新于
最后更新于
这就是两阶段提交,它实现了原子提交。两阶段提交在大量的将数据分割在多个服务器上的分片数据库或者存储系统中都有使用。两阶段提交可以支持读写多条记录,一些更特殊的存储系统不允许你在多条记录上支持事务。对于这些不支持事务中包含多条数据的系统,你就不需要两阶段提交。但是如果你需要在事务中支持多条数据,并且你将数据分片在多台服务器之上,那么你必须支持两阶段提交。
然而,两阶段提交有着极差的名声。其中一个原因是,因为有多轮消息的存在,它非常的慢。在上面的图中,各个组成部分之间着大量的交互。另一个原因是,这里有大量的写磁盘操作,比如说B在回复Yes给Prepare消息之后不仅要向磁盘写入数据,还需要等待磁盘写入结束,如果你使用一个机械硬盘,这会花费10毫秒来完成Log数据的写入,这决定了事务的参与者能够以多快的速度处理事务。10毫秒完成Log写磁盘,那么最快就是每秒处理100个事务,这是一个非常慢的结果。同时,事务协调者也需要写磁盘,在收到所有Prepare消息的Yes回复之后,它也需要将Log写入磁盘,并等待磁盘写入结束。之后它才能发送Commit消息,这里又有了10毫秒。在这两个10毫秒内,锁都被参与者持有者,其他使用相关数据的事务都会被阻塞。
这里我持续的在介绍性能,但是它的确非常重要,因为在一个繁忙的事务处理系统中,存在大量的事务,许多事务都会等待相同的数据,我们希望不要在一个长时间内持有锁。但是两阶段提交迫使我们在各个阶段都做等待。
进一步的问题是,如果任何地方出错了,消息丢了,某台机器崩溃了,如果你不够幸运进入到Block区间,参与者需要在持有锁的状态下等待一段长时间。
因此,你只会在一个小的环境中看到两阶段提交,比如说在一个组织的一个机房里面。你不会在不同的银行之间转账看到它,你或许可以在银行内部的系统中看见两阶段提交,但是你永远也不会在物理分隔的不同组织之间看见两阶段提交,因为它可能会陷入到Block区间中。你不会想将你的数据库的命运寄托在其他的数据库不在错误的时间崩溃,从而使得你的数据库被迫在很长一段时间持有锁。
因为两阶段提交很慢,有很多很多的研究都是关于如何让它变得更快,比如以各种方式放松这里的规则进而使得它变得更快,又比如对于一些特定的场景做一些定制化从而避免一些消息,我们在这门课中会看到很多这种定制。
两阶段提交的架构中,本质上是有一个Leader(事务协调者),将消息发送给Follower(事务参与者),Leader只能在收到了足够多Follower的回复之后才能继续执行。这与Raft非常像,但是,这里协议的属性与Raft又非常的不一样。这两个协议解决的是完全不同的问题。
使用Raft可以通过将数据复制到多个参与者得到高可用。Raft的意义在于,即使部分参与的服务器故障了或者不可达,系统仍然能工作。Raft能做到这一点是因为所有的服务器都在做相同的事情,所以我们不需要所有的服务器都参与,我们只需要过半服务器参与。然而两阶段提交,参与者完全没有在做相同的事情,每个参与者都在做事务中的不同部分,比如A可能在对X加1,B可能在对Y减1。所以在两阶段提交中,所有的参与者都在做不同的事情。所有的参与者都必须完成自己那部分工作,这样事务才能结束,所以这里需要等待所有的参与者。
所以,Raft通过复制可以不用每一个参与者都在线,而两阶段提交每个参与者都做了不同的工作,并且每个参与者的工作都必须完成,所以两阶段提交对于可用性没有任何帮助。Raft完全就是可用性,而两阶段提交完全不是高可用的,系统中的任何一个部分出错了,系统都有可能等待直到这个部分修复。比如事务协调者在错误的时间崩溃了,我们需要等待它上线并读取它的Log再重发Commit消息。如果一个参与者在错误的时间崩溃了,如果我们足够幸运,我们只需要Abort事务。所以实际上,两阶段提交的可用性非常低,因为任何一个部分崩溃都有可能阻止整个系统的运行。Raft并不需要确保所有的参与者执行操作,它只需要过半服务器执行操作,或许少数的服务器完全没有执行操作也没关系。这里的原因是Raft系统中,所有的参与者都在做相同的事情,我们不必等待所有的参与者。这就是为什么Raft有更高的可用性。所以这是两个完全不同的协议。
然而,是有可能结合这两种协议的。两阶段提交对于故障来说是非常脆弱的,在故障时它可以有正确的结果,但是不具备可用性。所以,这里的问题是,是否可以构建一个合并的系统,同时具备Raft的高可用性,但同时又有两阶段提交的能力将事务分包给不同的参与者。这里的结构实际上是,通过Raft或者Paxos或者其他协议,来复制两阶段提交协议里的每一个组成部分。
所以,在前面的例子中,我们会有三个不同的集群,事务协调器会是一个复制的服务,包含了三个服务器,我们在这3个服务器上运行Raft,
其中一个服务器会被选为Leader,它们会有复制的状态,它们有Log来帮助它们复制,我们只需要等待过半服务器响应就可以执行事务协调器的指令。事务协调器还是会执行两阶段提交里面的各个步骤,并将这些步骤记录在自己的Raft集群的Log中。
每个事务参与者也同样是一个Raft集群。
最终,消息会在这些集群之间传递。
不得不承认,这里很复杂,但是它展示了你可以结合两种思想来同时获得高可用和原子提交。在Lab4,我们会构建一个类似的系统,实际上就是个分片的数据库,每个分片以这种形式进行复制,同时还有一个配置管理器,来允许将分片的数据从一个Raft集群移到另一个Raft集群。除此之外,我们还会读一篇论文叫做Spanner,它描述了Google使用的一种数据库,Spanner也使用了这里的结构来实现事务写。