前一節的原子多播是一個更一般問題的特例:分散式提交(distributed commit)——讓某個操作由行程群組的每個成員都執行,或者全都不執行。在可靠多播中,這個操作是遞送一則訊息;在分散式交易中,則是參與交易的某個節點在本地提交交易。
分散式提交通常借助一位協調者(coordinator)。最簡單的方案是單階段提交協定(one-phase commit protocol):協調者直接告訴所有其他相關行程(稱為參與者,participant)要不要在本地執行該操作。明顯的缺陷是:參與者若無法執行,沒有管道告訴協調者——例如在分散式交易中,本地提交可能因違反並行控制約束而不可行。實務上需要更精緻的方案,最常用的是兩階段提交;其主要缺點是無法有效處理協調者失效,為此又發展出三階段提交。
兩階段提交(2PC)#
兩階段提交協定(two-phase commit protocol, 2PC)出自葛雷(Jim Gray)。考慮一個分散式交易,參與的行程各自跑在不同機器上。假設無失效時,協定分兩階段、共四步:
- 協調者向所有參與者送出 VOTE_REQUEST。
- 參與者收到 VOTE_REQUEST 後,回覆 VOTE_COMMIT(表示已準備好在本地提交自己那部分交易)或 VOTE_ABORT。
- 協調者收集所有投票:全數投提交,協調者才決定提交,並向所有參與者多播 GLOBAL_COMMIT;只要有一個參與者投中止,協調者就決定中止,多播 GLOBAL_ABORT。
- 投了提交票的參與者等待協調者的最終決定:收到 GLOBAL_COMMIT 就在本地提交,收到 GLOBAL_ABORT 就在本地中止。
第 1、2 步是投票階段(voting phase),第 3、4 步是決定階段(decision phase)。
失效下的 2PC:逾時處理#
協調者與參與者都有「阻塞等待訊息」的狀態,任何一方當機都可能讓其他行程無限期等下去,因此要加上逾時機制。

圖 8-18:(a) 2PC 中協調者的有限狀態機。(b) 參與者的有限狀態機。
從有限狀態機來看,共有三個阻塞點:
- 參與者在 INIT 狀態等 VOTE_REQUEST:逾時就直接在本地中止交易,並向協調者送 VOTE_ABORT。
- 協調者在 WAIT 狀態等所有投票:逾時就自己投中止,向所有參與者送 GLOBAL_ABORT。
- 參與者在 READY 狀態等全域決定:不能逕自中止——它必須查明協調者實際送出了什麼。最簡單的解法是阻塞到協調者復原為止;更好的做法是讓參與者 P 去聯絡另一個參與者 Q,看能否從 Q 的狀態推出該怎麼做:
- Q 在 COMMIT:協調者必然在當機前送過 GLOBAL_COMMIT(只是還沒送到 P),P 也可提交。
- Q 在 ABORT:P 可安全中止。
- Q 在 INIT:協調者是在多播 VOTE_REQUEST 途中當機的(送到了 P、沒送到 Q),P 與 Q 都可安全轉入 ABORT。
- Q 也在 READY:最棘手的情況。若查遍所有參與者都在 READY,無法做出任何決定——大家都願意提交,但少了協調者的一票就無法拍板,協定只能阻塞到協調者復原。

圖 8-19:參與者 P 處於 READY 狀態、並聯絡另一參與者 Q 之後所採取的行動。
「所有參與者都收下並處理了 VOTE_REQUEST,而協調者隨後當機」時,參與者無法合作做出最終決定,只能等協調者復原——因此 2PC 又稱阻塞式提交協定(blocking commit protocol)。
復原與日誌#
為了讓行程當機後能復原,各方必須把狀態存到持久儲存:
- 參與者在 INIT 當機:復原後可安全地在本地中止,再通知協調者。
- 參與者已在 COMMIT 或 ABORT:復原到該狀態,並向協調者重送自己的決定即可。
- 參與者在 READY 當機:復原後無法自行決定,必須聯絡其他參與者問結果——與 READY 逾時的處理相同。
- 協調者只需記兩個關鍵狀態:進入 WAIT 時記錄之(復原後可重送 VOTE_REQUEST);做出最終決定時記錄之(復原後可重送決定)。
延伸說明:協調者與參與者的執行流程
協調者:多播 VOTE_REQUEST 收集投票,記錄進入 WAIT,然後等票。若逾時仍沒收齊,就認定有參與者失效,中止交易並向(其餘)參與者多播 GLOBAL_ABORT。若無失效且全員(含協調者自己)投提交,先把 GLOBAL_COMMIT 寫入日誌再送出;否則先記錄再多播 GLOBAL_ABORT。

圖 8-20:兩階段提交協定中協調者所執行步驟的概要。
參與者:等待投票請求(可由行程位址空間內的獨立執行緒負責),沒等到就中止(顯然協調者失效了)。決定投提交時,先把決定寫入本地日誌,再送 VOTE_COMMIT,然後等全域決定;決定按時到達就寫入日誌並執行。若等待全域決定逾時,就執行終止協定(termination protocol):向其他行程多播 DECISION_REQUEST,然後阻塞等回應(回應也可能來自終將復原的協調者),收到後把決定寫入日誌並照辦。
處理他人的決定詢問:每個參與者另起一條執行緒,專門接收其他參與者的 DECISION_REQUEST。它只有在自己已達最終決定時幫得上忙——若本地日誌已寫入 GLOBAL_COMMIT 或 GLOBAL_ABORT,就回覆之;此外,若所屬參與者仍在 INIT,也可回覆 GLOBAL_ABORT(理由同前)。其他情況下無能為力,提問的參與者得不到回應。

圖 8-21:(a) 2PC 中參與者行程所執行的步驟。(b) 處理進來的決定詢問(decision request)的步驟。
避免阻塞有幾條路。其一(Babaoglu 與 Toueg):改用一種多播原語,接收者一收到訊息就立刻再多播給所有其他行程——可證明這讓參與者即使在協調者尚未復原時也能達成最終決定。其二就是接下來的三階段提交。
三階段提交(3PC)#
2PC 的問題在於協調者當機時參與者可能無法決定、被迫阻塞。Skeen 提出的**三階段提交協定(three-phase commit protocol, 3PC)**可在 fail-stop 當機下避免阻塞。
3PC 在文獻中被廣泛引用,實務上卻很少採用——因為 2PC 會阻塞的條件很少發生。討論它的價值在於加深對分散式容錯問題解法的理解。
3PC 同樣由協調者與參與者構成,其狀態機滿足兩個條件(Skeen 與 Stonebraker 證明這兩條件是提交協定非阻塞的充要條件):
- 不存在任何狀態能直接轉移到 COMMIT 或 ABORT。
- 不存在「無法做出最終決定、卻能轉移到 COMMIT」的狀態。
流程:協調者送 VOTE_REQUEST 給所有參與者並等回應。任一參與者投中止,最終決定即為中止,協調者送 GLOBAL_ABORT;若可以提交,協調者先送 PREPARE_COMMIT,等每個參與者都確認已準備好提交後,才送最終的 GLOBAL_COMMIT 真正提交交易。

圖 8-22:(a) 3PC 中協調者的有限狀態機。(b) 參與者的有限狀態機。
3PC 的阻塞點分析#
- 參與者在 INIT 等投票請求:逾時轉 ABORT(假定協調者已當),與 2PC 相同。
- 協調者在 WAIT 等投票:逾時判定有參與者當機,多播 GLOBAL_ABORT 中止交易。
- 協調者在 PRECOMMIT 逾時:某參與者當了,但已知它投過提交票,因此協調者可以放心指示其餘運作中的參與者提交(多播 GLOBAL_COMMIT),並仰賴復原協定讓當機的參與者將來復原時補提交自己那部分。
- 參與者 P 在 READY 或 PRECOMMIT 逾時(只能斷定協調者失效),P 聯絡其他參與者:
- 有人在 COMMIT(或 ABORT):P 跟著轉入該狀態。
- 所有參與者都在 PRECOMMIT:可以安全提交。
- 有參與者 Q 仍在 INIT:可以安全中止。注意 Q 在 INIT 就代表不可能有任何參與者在 PRECOMMIT——參與者要進 PRECOMMIT,協調者必須先進 PRECOMMIT,而那要求已收到每個參與者的提交票。
- P 能聯絡到的參與者都在 READY(且構成多數):中止交易。要點在於:可能還有一個參與者已當機、將來會復原,但沒有人知道它復原後會是什麼狀態。若它復原到 INIT,中止是唯一正確的決定;最壞情況它復原到 PRECOMMIT,此時中止也不會造成傷害。
- P 能聯絡到的行程都在 PRECOMMIT(且構成多數):可以安全提交。可證明此時其他行程要嘛在 READY,要嘛當機後將復原到 READY、PRECOMMIT 或 COMMIT。
這正是與 2PC 的關鍵差異:2PC 中一個當機的參與者可能復原到 COMMIT,而其他人全在 READY——導致存活者無法決定、只能等它復原。3PC 中只要有任何運作中的行程在 READY,就沒有任何當機行程會復原到 INIT、ABORT、PRECOMMIT 以外的狀態,存活的行程永遠能做出最終決定。