DDIA读书笔记 - 共识
FLP 结果 - 共识不可能性
The Impossibility of Consensus
Michael J. Fischer, Nancy Lynch, and Michael S. Paterson: “Impossibility of Dis‐
tributed Consensus with One Faulty Process,” Journal of the ACM, volume 32, num‐
ber 2, pages 374–382, April 1985. doi:10.1145/3149.214121
FLP 表明,在一个极其悲观、纯异步、确定性的模型中,共识是不可能的。FLP 不可能性结果并不意味着实际的分布式系统无法达成共识,真实分布式系统通过加入超时、时钟、随机性或最终同步性来绕过这一不可能性——因此共识在实践中是可以实现的,尽管并不总是立即实现共识。
关键在于模型假设:
- FLP 假设系统是纯异步的:消息延迟没有上界,没有同步时钟,也没有超时机制。
- 算法必须是确定性的。
- 至少有一个节点可能崩溃。
- 共识必须始终终止。
在这种模型中,无法可靠地区分一个已经崩溃的节点和一个只是非常慢的节点。一个缓慢或崩溃的节点就可能迫使算法永远等待,因此没有任何确定性算法能够同时保证共识的安全性和活性。但真实系统会放宽这些假设:
- 它们使用超时来怀疑节点失效,即使这种怀疑有时是错误的。
- 它们假设部分同步:网络可能在一段时间内是异步的,但最终消息会在某个时限内到达。
- 它们使用故障检测器,这些检测器可能不可靠,但最终会足够准确。
- 它们使用随机化,这打破了确定性对手论证,使共识能够以概率 1 达成。
- 它们依赖多数派 quorum、领导者选举、租约等机制。
因此,像 Paxos、Raft、Zab 和 Viewstamped Replication(视图戳复制)这样的实用共识算法可以达成共识,但通常有一个前提:它们始终保证安全性,而在网络分区、缓慢时期或错误的故障怀疑期间,活性可能会被延迟或暂时丧失。
Atomic Commit and Two-Phase Commit (2PC)
事务ACID特性的原子性的目的,是在若干个写入操作执行到一半时出了问题时,提供简单的语义:事务的结果要么是成功提交,此时事务的所有写入都被持久化;要么是中止,此时事务的所有写入都被回滚(即撤销或丢弃)。
原子性可以防止失败的事务用半成品结果和半更新状态把数据库弄乱。这对于涉及多表多行的更新事务以及数据库二级索引的维护来说尤其重要,例如:每个二级索引都是独立于主数据的数据结构,如果你修改了某些数据,相应的更改也需要在二级索引中同步进行。原子性确保二级索引与主数据保持一致(如果索引与主数据变得不一致,那索引就不会很有用,甚至是有问题的)。
单节点的场景,原子性由单节点上的数据库实例保证,通常是通过 WAL 日志的方式实现,而分布式场景的原子性可以通过 2PC 来实现
如果一个事务涉及多个节点,或者维护一个按某个字段分区的二级索引(其中索引条目可能与主数据位于不同节点)。在这些情况下,仅仅向所有节点发送提交请求,并让每个节点独立提交事务是不够的。这样做很容易出现提交在某些节点上成功、而在另一些节点上失败的情况,从而违反原子性保证。
- 有些节点可能检测到约束违反或冲突,从而必须中止,而其他节点却能够成功提交。
- 有些提交请求可能在网络中丢失,相关节点最终因超时而中止,而另一些提交请求则成功送达。
- 有些节点可能在提交记录完全写入之前崩溃,并在恢复时回滚,而其他节点则成功提交。
事务提交必须是不可撤销的。一旦事务已经提交,就不允许改变主意并追溯性地中止它。这条规则的原因是:一旦数据被提交,它就对其他事务可见,因此其他客户端可能开始依赖这些数据;这一原则也是读已提交这个隔离级别的基础。如果允许事务在提交后中止,那么任何读取了已提交数据的事务都将基于那些被追溯宣布为不存在的数据——因此它们也必须被回滚。
两阶段提交(2PC)是分布式数据库中的一种经典算法,用于跨多个节点实现原子事务提交。
下面是 2PC 的基本工作流程:
- 协调者(coordinator / transaction manager):单节点事务不需要它,但跨多个节点时需要。它负责问所有节点“能不能提交”,收集投票,然后决定最终是提交还是中止。协调者可以是一个库,嵌在应用进程里,比如 Java EE 容器中的事务管理器;也可以是独立进程或服务,比如 Narayana、JOTM、BTM、MSDTC。
- 参与者(participants):就是参与这个事务的各个数据库节点。应用在这些节点上正常读写数据。

假设应用要同时在数据库 A、数据库 B 上做修改。
第一阶段:prepare / 投票阶段
应用觉得可以提交了,于是通知协调者。协调者进入第一阶段:
- 协调者向每个参与者发送 prepare 请求。
- 每个参与者收到后,会做本地准备工作:执行事务中的写操作,但还不真正提交;检查约束、冲突;写日志;锁住相关数据;然后回复协调者:yes 或 no。yes 的意思是:我这里没问题,可以提交,但我先不提交,等你最终通知。no 的意思是:我这里有问题,必须中止。
协调者收集所有参与者的回复。
第二阶段:执行决定
如果所有参与者都回复 yes:协调者决定提交,向所有参与者发送 commit 请求。参与者收到后真正提交,事务完成。
如果任何一个参与者回复 no:协调者决定中止,向所有参与者发送 abort 请求。所有参与者回滚本地事务。
2PC 的问题
共识问题
2PC本质上是一个试图解决“共识问题”的协议,但它是一个不完善的共识协议。它试图让所有参与者对“提交(Commit)”或“中止(Abort)”这个全局决定达成一致。但是 2PC 在处理故障时,无法完美解决共识问题。
一个完美的共识算法必须满足三个属性:
- 一致性(Agreement):所有节点决定相同的值。
- 有效性(Validity):决定的值必须是之前某个节点提议的(不能凭空捏造,比如不能随便选一个无关的值)。
- 终止性(Termination):所有未崩溃的节点最终都能做出决定。
2PC 满足了前两个,但在第三个属性(终止性)上存在致命缺陷。
缺陷的核心:协调者单点故障(阻塞问题)
如果参与者或网络在过程中出现故障,:如果任何准备请求失败或超时,协调器将中止事务操作;如果任何提交或中止请求失败,协调器将无限期重试它们。如果协调器崩溃,情况会如何则不太清楚。
如果协调器在发送准备请求之前失败,参与者可以安全地中止事务。但一旦参与者收到准备请求,并且投了“赞成”票后,它不能再单方面中止,必须等待对方的回复:协调器判断事务是已提交还是已中止。如果协调器此时发生崩溃或网络故障,参与者只能等待。

当数据库1和数据库2回复 yes 后,它们进入了“不确定状态”(in-doubt)。
此时如果协调者崩溃了(比如在发送 commit 之前),数据库1和数据库2既不能提交,也不能中止。因为它们不知道其他节点投了什么票,也不知道协调者最终的决定是什么。如果它们擅自决定,就可能与协调者的最终决定(或者与其他节点的决定)冲突,导致不一致。
结果:参与者只能一直持有锁(图中的灰线),无限期等待协调者恢复。这就违反了共识的终止性(系统被阻塞,无法继续推进)。
“脑裂”与错误怀疑
在没有超时机制的情况下,如果引入超时(部分同步假设),节点可能会错误地怀疑协调者已死。
如果部分参与者超时后自行决定提交,而另一部分参与者超时后自行决定中止,就会发生脑裂,彻底破坏一致性。
性能问题
顺便提一下,2PC带来了多轮网络交互以及极长的锁持有时间,这也是为什么在追求高性能的分布式系统中,2PC 往往会成为瓶颈,甚至引发宕机连锁反应的原因。
工程实践
由于 2PC 本身无法完美解决共识,分布式数据库是如何在实践中会把 2PC 的协调者做成一个高可用的共识组,这被称为 Paxos Commit 或 基于 Raft 的 2PC:
不再使用单点协调者:协调者的状态(事务日志、投票结果)不再只存在一台机器上,而是通过 Paxos 或 Raft 协议复制到一个集群中。
- 协调者高可用:如果主协调者崩溃,Raft/Paxos 会迅速选举出新的主协调者。
- 恢复决定:新协调者上任后,读取复制日志,得知之前的投票结果,然后继续完成 commit 或 abort 的广播。
- 结果:只要多数派协调者节点存活,2PC 的“终止性”就能得到保证,共识得以达成。
学习资料
- 《Designing Data-Intensive Applications 2nd Edition》,Distributed Transactions and Consensus 章节。中文翻译:https://github.com/vonng/ddia
- Raft 协议官方网站:https://raft.github.io/
- 网页动画学习Raft:https://thesecretlivesofdata.com/raft/
- https://www.zhihu.com/question/37647788
- Raft 算法的原作者论文:In Search of an Understandable Consensus Algorithm
- 《深入理解分布式系统》- 唐伟志

浙公网安备 33010602011771号