故事
光绪末年,京张铁路修到居庸关,总工程师詹天佑在关沟段设了三座号志站:南口、青龙桥、八达岭。火车要爬坡,三站必须同时扳道岔、对信号,稍有差池就是坠崖。
詹天佑的副手是个留英回来的年轻人,姓马,熟读斯凯奇的书,嫌老式的"两声鼓"确认太莽撞——司机只听"预备"和"发车"两声令,中间没有回旋余地,万一第二声鼓响到一半电报线被风刮断,前面车站发了车,后面车站没收到,两车对开就完了。
马工程师加了一道"预预备":第一声鼓问"能不能发车",各站检查自身状态;第二声鼓问"准备好了没有",各站锁定道岔;第三声鼓才是"发车"。三声鼓之间,任何一站发现异常都能喊停,不必等第三声鼓落槌。
这叫三阶段提交:CanCommit、PreCommit、DoCommit,给两阶段中间塞一层缓冲,让"准备"和"真正动手"之间多一次反悔的机会。
—
居庸关的号志员老周头干了二十年,第一声鼓响的时候他照例检查风门、水柜、制动,回复"能"。第二声鼓响,他扳死道岔、挂上锁销,回复"准备好了"。这时候他的状态已经变了——道岔锁死,对向列车进不来,本务机车也退不回去。如果第三声鼓永远不来,他就得举着灯在月台上干等,等到调度所解除锁定。
马工程师的算盘是:第二声鼓之后、第三声鼓之前,如果调度所发现某站失联,可以统一发"撤销",让所有站退回第一声鼓之前的状态。这比两阶段提交优雅——两阶段里一旦"准备"完成、"提交"卡住,参与者就悬在半空,锁死不释放。
老周头问过一个刁钻的问题:要是第二声鼓之后,调度所发"撤销",青龙桥收到了,八达岭没收到呢?马工程师说,八达岭超时未收"撤销",会自动按原定的"提交"执行——反正多数站已经收到"撤销",少数站按"提交"走,事后对账总能发现。
老周头没再追问。他知道马工程师回避了一个更狠的情形。
—
宣统元年秋天,暴雨冲断关沟段的电报线,正好在第二声鼓之后、第三声鼓之前。南口收到"撤销",青龙桥收到"撤销",八达岭的线断了,什么都没收到。按马工程师的规则,八达岭超时自动"提交",扳道发车;南口和青龙桥按"撤销"处理,道岔解锁。结果八达岭的货车冲下青龙桥方向的坡,而青龙桥已经按"撤销"把对向道岔接进了侧线——货车直直扎进空车厢堆里,死伤三十余人。
事后调查,马工程师的"三声鼓"在纸面上确实比"两声鼓"多一层保险:正常网络下,任何单点故障都能在第二声鼓之后被"撤销"挽回。可一旦网络分区——线断成两截,两边各自演化——三阶段和两阶段一样 helpless。八达岭的"超时提交"和南口的"收到撤销"之间没有仲裁者,因为仲裁者(调度所)自己也分裂了:它给南口发了"撤销",给八达岭的报文却丢在路上。
詹天佑在事故报告里写了一句后来被反复引用的话:"网络分区之下,没有完美的提交协议。" 这不是说三阶段提交毫无价值——正常网络里它确实降低了阻塞窗口——而是说,它修复的是两阶段提交的症状,不是病根。病根在 FLP 不可能性里早就写死了:异步网络中,没有算法能同时保证安全性和活性。
—
马工程师后来去了交通部,三阶段提交的思想却被计算机科学家重新发现。Skeen 在 1982 年提出非阻塞提交协议时,用的正是同样的三层结构:先问意愿,再锁资源,最后落槌。Lampson 和 Lomet 在 1993 年的论文里把它形式化,证明三阶段提交在同步网络(有明确超时上限)里可以避免阻塞——可这个前提本身就是最大的妥协:真实网络从来不敢承诺"一定在 T 秒内送达"。
更讽刺的是,三阶段提交为了这层"可撤销"的缓冲,付出了额外的延迟和更复杂的故障恢复。两阶段提交卡住时,参与者悬在半空,但至少状态明确;三阶段提交的参与者在第二声鼓之后、第三声鼓之前,既可能提交也可能撤销,协调者崩溃后新上任的协调者根本无从判断该走哪条路——它得去逐个询问参与者"你当时到底收到第三声鼓没有",这和两阶段提交的阻塞恢复一样狼狈。
现代数据库几乎不用纯粹的三阶段提交。Spanner 用两阶段提交加 Paxos 组内复制,把协调者的可用性交给共识算法保证;Percolator 用预写日志和乐观锁,干脆绕过提交协议的阻塞问题。三阶段提交留在教科书里,作为一则警示寓言:当你看到某个协议声称"修复了 X 的所有问题",先问它假设了什么网络模型——如果假设的是"不分区的网络",那它修复的就不是分布式系统里的真问题。
概念解析
三阶段提交(3PC)把两阶段的"准备-提交"拆成"CanCommit-PreCommit-DoCommit",在 PreCommit 之后引入一个可撤销的窗口。设计意图是:协调者若在 PreCommit 后崩溃,新协调者可以统一决策"全部提交"或"全部撤销",避免参与者无限阻塞。
这个设计在无网络分区的同步模型里确实成立,但 FLP 不可能性决定了:一旦允许网络分区或消息延迟无上限,3PC 和 2PC 面临同样的困境——分区两侧可能各自演化出不可调和的状态。3PC 的第三声鼓若只到达部分参与者,超时机制会让未收到者自动推进,结果与协调者的真实意图背离。
工程实践中,3PC 的额外往返延迟和故障恢复复杂度让它很少被直接采用。更常见的路线是:要么接受 2PC 的阻塞风险但通过共识算法保证协调者高可用(Spanner),要么改用 Saga、TCC 等补偿型事务绕过原子提交的硬约束。3PC 的价值不在实用,而在它把"分布式事务的不可能性"摊得更开——多一层缓冲,多一次看清:网络分区是绕不过去的墙。