01

一致性的幻覺:線性一致性

為什麼多副本的資料庫,能讓你「感覺」它只有一份資料——以及這個假象什麼時候會被拆穿

同時問兩個副本同一個問題,卻拿到兩個答案

在會「最終一致」的資料庫裡,如果你同時問兩個不同的副本同一個問題,可能會得到兩個不同的答案。這很令人困惑——如果資料庫能營造出「其實只有一份資料」的假象,那不是簡單多了嗎?

這正是 線性一致性(linearizability) 的核心想法。它的基本概念很簡單:讓系統表現得好像只有一份資料副本,而且對它的所有操作都是原子的。

1
寫入即公告

一旦某個客戶端成功完成一次寫入,所有正在讀取的客戶端都必須能看到剛寫入的值。

2
讀到的一定最新

你讀到的值一定是最新、最即時的值,不會來自過期的快取或落後的副本——這就是 新鮮度保證(recency guarantee)

3
不能開倒車

一旦有任何一次讀取回傳了新值,之後所有的讀取(不論在哪個客戶端)都必須也回傳新值,不能再倒退回舊值。

📌
先給你一個畫面

想像全公司只有一張公告欄。一旦有人把新公告貼上去,任何之後來看的人都只會看到新公告,不可能看到被蓋掉的舊紙條。多副本資料庫背後其實有很多張紙,但線性一致性讓它看起來就像只有這一張公告欄。

直接讀原文:世界盃比分為什麼讓球迷一頭霧水

書裡用一個真實網站的例子,帶出「不是線性一致」到底長什麼樣子。一句對一句,左邊原文、右邊白話。

原文 · DDIA Ch.9 Wouldn't it be a lot simpler if the database could give the illusion that there is only one replica (i.e., only one copy of the data)? In a linearizable system, as soon as one client successfully completes a write, all clients reading from the database must be able to see the value just written. Maintaining the illusion of a single copy of the data means guaranteeing that the value read is the most recent, up-to-date value, and doesn't come from a stale cache or replica. In other words, linearizability is a recency guarantee. Bob knows that he hit the reload button after he heard Alice exclaim the final score, and therefore he expects his query result to be at least as recent as Alice's.
白話翻譯

如果資料庫能營造出「其實只有一份副本(也就是只有一份資料)」的假象,那不是簡單多了嗎?

在一個線性一致的系統裡,只要有一個客戶端成功完成寫入,所有正在讀取這個資料庫的客戶端,都必須能看到剛剛寫進去的值。

要維持「只有一份資料」的假象,就得保證讀到的值是最新、最即時的,不能來自過期的快取或落後的副本。

換句話說,線性一致性是一種新鮮度保證。

Bob 知道自己是在聽到 Alice 喊出比分之後才按下重新整理的,所以他預期自己查到的結果,至少要跟 Alice 看到的一樣新。

重點不是「同時」,是「先後」

如果 Alice 和 Bob 同時重新整理,拿到不同結果反而不奇怪——他們根本不知道對方的請求何時被伺服器處理。但 Bob 的動作明明發生在 Alice 之後,卻看到更舊的世界。這正是違反線性一致性的關鍵:後發生的操作,不該看到比先發生的操作更舊的狀態。

互動重播:手機、電視、還有那個「幻覺」

下面用世界盃比分的經典場景,演給你看「沒有線性一致性」會多讓人困惑,然後看線性一致性怎麼把這個漏洞補起來。按「下一步」往下播。

比分寫入來源
📱
Alice 的手機(副本 A)
📺
Bob 的電視(副本 B)
按「下一步」開始這趟旅程
😵
問題不是「有沒有複製」,是「新不新鮮」

Bob 看到舊比分,不是因為系統沒有複製資料,恰恰是因為多個副本、而其中一個落後了。線性一致性要解決的,正是這種「同一時刻不同地方看到不同事實」的一頭霧水。

用「暫存器」想清楚:並行讀取能回傳什麼?

分散式系統文獻把單一個鍵叫做 暫存器(register) ,它可以是 key-value store 的一個鍵,也可以是關聯式資料庫的一列。假設 x 一開始是 0,某客戶端正在把它寫成 1:

在寫入開始前完成的讀取

一定得回傳舊值 0——這時候新值根本還沒發生。

在寫入完成後開始的讀取

一定得回傳新值 1——這時候寫入已經生效了。

與寫入時間重疊的讀取

0 或 1 都合法——我們想像在寫入期間有個瞬間,值原子地從 0 翻成 1。

這也是它常被搞混的地方:線性一致性很容易跟 可序列化(serializability) 搞混,因為兩個詞看起來都像「可以排成某種順序」。但可序列化管的是多筆交易要不要對得起帳,它不要求順序等於真實時間;線性一致性管的是單一物件讀寫夠不夠新鮮,而且死死綁住真實時間的先後。兩者都要的組合,書中叫 嚴格可序列化(strict serializability)

🎬
結帳排隊 vs 現場直播

可序列化像超市結帳:只要帳目能排成某個合理順序就好,誰先誰後不重要。線性一致性像看同一場現場直播:只有一個畫面,一旦比分更新,所有在那之後看的人都必須看到新比分——真實時間的先後才是重點。

看比分過期沒關係,但有些事真的不能錯

看球賽比分晚個幾秒沒什麼大不了,但書中舉了幾類「少了線性一致性就會出事」的情境:

🔒
上鎖與領導者選舉

單主複製必須確保只有一個領導者,否則就會 腦裂(split brain) 。不論鎖怎麼實作,它都必須是線性一致的:所有節點必須對「誰持有鎖」有一致的看法。

🎫
約束與唯一性保證

帳號名稱、電子郵件都要求唯一。兩人同時搶同一個名稱,必須剛好只有一人成功——這需要 線性一致性

🏃
跨通道的時序依賴

系統裡有多個溝通通道時,缺乏新鮮度保證會造成競態條件——像照片先寫進檔案儲存,縮圖指令再丟進訊息佇列,兩條通道賽跑。

🎟️
搶最後一個座位

一場演唱會只剩最後一個座位,兩位顧客同時點擊「訂位」。系統必須保證只有一個人訂到,另一個人收到「已售完」——這就像帳號註冊:用原子比較並設定,把名稱設成那位使用者的 ID,前提是它還沒被佔走。

但不是所有約束都得這麼硬:硬性唯一約束(如關聯式資料庫的 unique)需要線性一致性;可寬鬆對待的約束則不一定——例如機位超賣時,可以事後把客人改到別班機並給補償。外鍵、屬性約束等其他約束,通常不需要線性一致性也能實作。像 ZooKeeper、etcd 這類 協調服務 ,就是靠共識演算法以容錯方式提供這種線性一致操作。

動手配配看:這些情境,需要線性一致性嗎?

把下面幾種情境拖到正確的分類。配完按「對答案」。

演唱會搶最後一位——唯一性約束的搶票/選課系統
分散式鎖/領導者選舉
一般社群動態時間軸顯示
需要線性一致性:所有節點必須對「單一最新狀態」有一致看法,否則兩人可能同時搶到同一個名額,或同時出現兩個領導者
拖到這裡
不需要,最終一致性就夠了:晚個幾秒看到別人的新貼文,不會造成任何實質損害
拖到這裡
🧃
現榨果汁 vs 冰箱果汁

線性一致性的讀取就像每次都喝到現榨的果汁,保證最新鮮;最終一致性則像冰箱裡放了幾天、可能已經不新鮮的果汁——雖然最後會換新,但你不知道何時。動態時間軸可以喝「冰箱果汁」,搶票系統不行。

小試身手

把「什麼是線性一致性」跟「什麼時候真的需要它」再檢查一次,來兩題。

一句話總結,線性一致性最核心的保證是什麼?
兩位使用者同時嘗試註冊同一個帳號名稱,系統要保證只有一人成功。這個需求最接近下列哪種操作?
⚖️
下一站:這麼好的保證,代價是什麼?

線性一致性聽起來像萬靈丹,但它跟系統的可用性、跟網路分區之間,藏著一個沒辦法兩者兼得的取捨。往下捲,我們去看看這筆帳到底要怎麼算。

02

線性一致性的代價

強一致性聽起來很美好——但天下沒有白吃的午餐,這一站我們把帳單翻出來看清楚

只用一份資料,最簡單也最脆弱

上一站我們認識了 線性一致性 ——系統表現得「像只有一份資料」。最簡單的做法,就是真的只留一份。但這樣完全無法容錯:那台節點一掛,資料就沒了,或至少暫時拿不到。

所以實務上得靠 複製 來容錯。但複製一多,「線性一致」就不是預設值了——四種主流複製方法,命運完全不同:

單主複製

從領導者或同步追隨者讀,有潛力線性一致——但用了快照隔離、有並行錯誤,或誤把舊領導者當領導者,就會破功。

共識演算法

ZooKeeper、etcd 這類系統內建防止腦裂與過期副本的機制,能安全提供線性一致儲存。

多主複製

多個節點並行處理寫入、再非同步交換,會產生衝突,不是線性一致。

無主複製

基於時鐘的「最後寫入勝出」,大概不是線性一致——時鐘會偏移,靠不住。

📢
單主複製像唯一發言人

公司只有一位官方發言人(領導者),所有對外消息先經過他,再轉達給助理(追隨者)。你直接問發言人、或問已同步過的助理,答案就是最新的。但如果助理還沒同步、你卻去問他,就可能拿到舊消息——這就是為什麼不是「每一個」單主資料庫都線性一致。而多主複製像每個分公司都有自己的發言人,各自接消息、事後才互相同步,結果可能互相矛盾,需要事後解衝突——沒有單一份真相,本質上就無法線性一致。

🎯
連「嚴格法定人數」也救不了

常有人以為在無主系統裡,只要滿足 法定人數 w + r > n 就穩了。但書中給了反例:寫者把 x 從 0 改成 1、送往三個副本(n=3, w=3);客戶端 A 讀兩個節點看到新值 1;客戶端 B 在 A 完成之後才開始讀、卻讀到另兩個節點,拿回舊值 0。法定人數條件明明滿足,B 卻比 A 看到更舊的值——順序就這樣亂了。要補救,讀者得同步做讀取修復、寫者得先讀過法定節點才寫,但這要付出效能代價,而且比較並設定(cas)這種操作靠這招也做不到,它需要共識演算法才行。

直接讀原文:CAP 定理到底在講什麼

兩個資料中心之間網路斷了,會發生什麼事?書裡用這個場景,把後來被稱為 CAP 定理的洞見講得很清楚。

原文 · DDIA Ch.9 If your application requires linearizability, and some replicas are disconnected from the other replicas due to a network problem, then some replicas cannot process requests while they are disconnected: they must either wait until the network problem is fixed, or return an error — either way, they become unavailable. If your application does not require linearizability, then it can be written in a way that each replica can process requests independently, even if it is disconnected from other replicas. In this case, the application can remain available in the face of a network problem, but its behavior is not linearizable. Thus, applications that don't require linearizability can be more tolerant of network problems. This insight is popularly known as the CAP theorem, named by Eric Brewer in 2000. CAP is sometimes presented as Consistency, Availability, Partition tolerance: pick 2 out of 3. Unfortunately, putting it this way is misleading, because network partitions are a kind of fault, so they aren't something about which you have a choice: they will happen whether you like it or not. A better way of phrasing CAP would be either Consistent or Available when Partitioned.
白話翻譯

如果你的應用需要線性一致性,而部分副本因網路問題連不到其他副本,那斷線的那些副本就不能處理請求——它們只能等網路修好,或直接回錯誤,兩種結果都是「變得不可用」。

如果你的應用不需要線性一致性,就可以讓每個副本各自獨立處理請求,即使跟其他副本斷線也照樣運作。這時應用在網路故障下能保持可用,但行為就不是線性一致的了。

所以,不需要線性一致性的應用,能更耐得住網路問題。這個洞見就是廣為人知的 CAP 定理,由 Eric Brewer 在 2000 年命名。

CAP 有時被講成「一致性、可用性、分區容忍,三選二」。可惜這種講法很誤導人,因為網路分區是一種故障,不是你能選擇要不要的東西——它就是會發生,不管你喜不喜歡。

更貼切的講法應該是:分區發生時,你只能二選一——保一致,或保可用。

☎️
兩家分店與總部斷線的電話

連鎖店總部(領導者)與分店之間的電話線斷了。堅持一致的分店規定「沒問過總部不准賣限量商品」——電話一斷,就只能停賣那項業務,保住一致、犧牲可用。堅持可用的分店說「先賣再說,等通了再對帳」——照常營業,但可能兩家分店把同一件限量品都賣掉了,保住可用、犧牲一致。最關鍵的體悟是:電話線會不會斷,不是你能選的——它就是會斷。

點點看:CAP 三角的三個角

把 CAP 定理的三個字拆開,點下面三個角,看每一個到底在講什麼白話意思。

C · 一致性
A · 可用性
P · 分區容忍
C · 一致性(Consistency,此處特指線性一致性):系統表現得像只有一份資料,任何時候讀到的都是最新值,絕不會讀回過期的舊資料。代價是:一旦網路分區發生,連不到多數節點的那端就必須拒絕請求或等待,直到能確認自己拿到的是最新狀態。
⚠️
最常見的誤解,一定要糾正

很多人把 CAP 講成「C、A、P 三個裡面選兩個」,聽起來 P 好像是可以放棄的選項——這是誤導。P(網路分區)不是你能選擇要不要的選項——分區會不會發生,不是系統設計者能決定的事,你唯一能選的,是分區真的發生時,要犧牲 C 還是犧牲 A。網路正常時,系統可以同時擁有一致性與可用性;只有分區真的發生的那一刻,才被迫二選一。更貼切的講法是「分區時:保一致還是保可用(Consistent or Available when Partitioned)」。

而且 CAP 的正式定義範圍其實很窄:只考慮一種一致性模型(線性一致)和一種故障(網路分區),對網路延遲、節點當機這些其他重要的取捨隻字不提。「可用性」本身的定義也有多種矛盾說法。所以書裡的結論很直接——CAP 雖然有歷史影響力,對實際設計系統的幫助卻很有限,最好避免用它來下結論。

為什麼線性一致性天生就慢?

更讓人意外的是:實務上很少有系統真的是線性一致的——就連現代多核 CPU 的 RAM 都不是!如果一個核心寫入某個記憶體位址,另一個核心隨後讀取同一位址,並不保證讀到剛才寫入的值(除非用了 記憶體屏障 )。

📝
桌上的便利貼

每個 CPU 核心都有自己的 快取 ,像員工桌上的便利貼——查便利貼很快,但可能不是最新的。員工改了數字,先改自己的便利貼,晚點才更新總倉的白板(主記憶體),於是同一個數字在不同人桌上、白板上可能都不一樣。

🏃
每次都跑去總倉確認

若要保證線性一致,每次讀寫都得親自跑一趟總倉的白板確認最新值——這當然準,但慢得多。而且這個慢是隨時都慢,不只是網路出問題的時候才慢。

放棄線性一致性的真正理由是效能,不是容錯——用 CAP 定理去解釋多核記憶體模型根本說不通:一台電腦內部我們假設通訊可靠,也不會期待一個核心斷線了還能繼續正常運作。下面這個流程,說明「線性一致的讀寫」為什麼比較弱一致性的讀寫慢:

🖥️
客戶端
⚙️
協調節點
🗳️
多數複本
按「下一步」開始
📐
這不是猜的,是被證明過的

Attiya 與 Welch 證明了:若你要線性一致,讀寫請求的回應時間至少正比於網路延遲的不確定性。沒有更快的線性一致演算法存在,但較弱的一致性模型可以快很多。系統對延遲敏感(如即時互動)、或要跨地理區域部署時,這個取捨格外重要——若業務能接受較弱一致性,往往能換來顯著的效能提升。

小試身手

從複製方法的取捨、到 CAP 的正確理解,來檢查一下有沒有抓對重點。

關於 CAP 定理裡的「P(分區容忍)」,下列哪個說法最準確?
現代多核 CPU 的 RAM 其實不是線性一致的(除非用記憶體屏障)。書中認為放棄線性一致性的「真正理由」是什麼?
🔗
下一站:如果不能「即時看到最新」,至少要保證什麼?

線性一致性又慢又貴,很多系統乾脆不追求它。但完全不管順序也不行——那至少要保證什麼?答案是:順序,尤其是有因果關係的順序,不能亂。往下捲,看看「因果一致性」怎麼用更便宜的方式,守住這條底線。

03

順序的保證:因果關係與全序廣播

先搞懂「誰先誰後」為什麼重要,再看時間戳與廣播怎麼把順序「釘死」

順序為什麼一再出現:因為要保留「因果」

這一整本書反覆繞回「順序」這個主題,其中一個核心原因是:順序有助於保留 因果關係(causality)。 想想幾個你早就知道的常識:問題一定先於答案(回答的人一定先看過問題);一列資料必須先被建立,才能被更新;訊息必須先被送出,才能被接收。

對任兩個操作 A 與 B,其實只有三種可能:A 發生在 B 之前、B 發生在 A 之前,或者 A 與 B 是 並行(concurrent) 的——彼此互不知道對方。這就是 happened-before 關係, 也是因果的另一種說法:若 A 發生在 B 之前,代表 B 可能知道 A、建立於 A 之上、或依賴 A。

A 先於 B

B 可能知道 A、建立於 A 之上,或依賴 A——兩者之間有因果連結。

B 先於 A

方向反過來,一樣有因果連結,只是誰是因、誰是果對調了。

A 與 B 並行

沒有因果連結——我們能確定的是,兩邊都不知道對方的存在。

🌿
因果順序像 Git 歷史

用過 Git 就懂:一個 commit 常常接在另一個之後,排成一直線;但有時會出現分支(多人同時開發),之後再合併起來。這正是 部分順序(partial order) 的樣子——不是每兩個事件都能比出先後,正如集合 {a, b} 和 {b, c} 誰也不是誰的子集。相對地,自然數是 全序(total order) ——5 和 13 你一定說得出誰大。線性一致性給的是全序(單一時間線);因果關係給的是部分順序(並行事件無法比較,就像 Git 的並行分支)。

🛡️
線性一致性其實更強

若系統遵守因果關係施加的順序,就說它是 因果一致(causally consistent) 的——例如快照隔離就提供因果一致:讀到某筆資料時,也必能讀到因果上先於它的資料。而線性一致性蘊含因果關係:任何線性一致的系統都會自動、正確地保留因果,這正是它好理解的原因。但好消息是,線性一致性不是保留因果的唯一辦法——因果一致是「不會被網路延遲拖慢、且分區時仍可用」的最強一致性模型,CAP 定理對它並不適用。

直接讀原文:happened-before 與因果

這一段是本節最關鍵的定義,我們把它原文放左邊,白話放右邊,一句對一句讀。

原文 · DDIA Ch.9 If you have two operations A and B, there are three possibilities: either A happened before B, or B happened before A, or A and B are concurrent. This happened before relationship is another expression of causality: if A happened before B, that means B might have known about A, or built upon A, or depended on A. If A and B are concurrent, there is no causal link between them; in other words, we are sure that neither knew about the other. A transaction reads from a consistent snapshot — consistent with causality: if the snapshot contains an answer, it must also contain the question being answered.
白話翻譯

如果你有兩個操作 A 和 B,只有三種可能:A 發生在 B 之前、B 發生在 A 之前,或者 A 與 B 是並行的。

這個 happened-before 關係,其實是因果的另一種說法:若 A 先於 B,代表 B 可能知道 A、建立於 A 之上、或依賴 A。

如果 A 與 B 並行,兩者之間就沒有因果連結——換句話說,我們確定雙方都不知道對方存在。

交易讀到的是一個「一致的快照」——這裡的一致指的是與因果一致:如果快照裡有一個答案,它也必須包含那個被回答的問題。

🎲
各自編號會出亂子

想像兩個團隊各自為自己的工作編號:A 隊只用奇數、B 隊只用偶數。問題是兩隊的速度不同,A 隊的奇數計數器可能遠遠領先 B 隊的偶數計數器——拿一個奇數工作和一個偶數工作比,你根本說不準誰先發生。這正是「非因果的序號產生器」的毛病:奇偶數分配、附上實體時鐘戳記、預先配置號碼區塊,這幾種常見做法產生的序號,統統都不與因果一致。

動手看:Lamport 時間戳怎麼「見面就對錶」

追蹤每一條因果依賴太貴了——更好的辦法是用 邏輯時鐘(logical clock) 替每個操作編號。 Lamport 時間戳 的妙處就在一條規則:每個節點都記住自己見過的最大計數器值,並把它附在每次請求上;一旦收到的值比自己大,就立刻把自己的計數器跳到那個值。三個節點各自的實體時鐘完全不同步也沒關係——按「下一步」看它怎麼運作。

🖥️
節點 A
🖥️
節點 B
🖥️
節點 C
按「下一步」看時間戳怎麼傳遞

Lamport 時間戳長相很簡單:是一對 (計數器, 節點 ID)——先比計數器,計數器相同就比節點 ID,保證每個時間戳都唯一。

📇
跟版本向量不一樣

Lamport 時間戳只負責強制出一條全序,卻分辨不出兩個操作到底是並行、還是有因果關係——它只是硬把它們排成一條線。想同時看出「誰跟誰並行」,得用 版本向量(version vector) ,代價是它比 Lamport 時間戳大、記的東西更多。

Lamport 時間戳留下的缺口

光有一個「與因果一致的全序」,還不足以解決像帳號名稱唯一這類問題:因為你不知道這個全序何時定案。要實作這種約束,你必須確定沒有別的節點能在全序中搶到你前面——而 Lamport 時間戳給不了這個保證,因為隨時可能冒出一個更小的時間戳,事後插隊到你前面。

這個「知道全序何時定案」的概念,就是 全序廣播(total order broadcast) ,又稱原子廣播。

📡
可靠傳遞 Reliable delivery

沒有訊息遺失:只要一則訊息傳給了一個節點,就一定會傳給所有節點。網路中斷時訊息暫時送不出去,但修好後仍要補送。

📻
全序傳遞 Totally ordered delivery

訊息以完全相同的順序,傳遞給每一個節點——即使節點或網路出錯,這個順序保證也必須永遠成立。

📻
像所有人同步收聽的電台

想像一個所有節點都收聽的廣播電台,主持人依序唸出一條條訊息:可靠傳遞是「只要唸出來,所有聽眾都會聽到,不會有人漏掉」;全序傳遞是「每位聽眾聽到的順序完全一樣」。關鍵在於,順序在訊息傳遞時就固定了——主持人不能事後把一條訊息硬塞到已經播出訊息的前面。這正是全序廣播比時間戳排序更強大的地方:時間戳排序可能事後才發現有更早的操作要插進來,但廣播一旦播出,就不能反悔。換個角度看,全序廣播就像在附加寫入一份共用日誌:傳遞一則訊息,等於在日誌尾端加一筆,所有節點讀同一份日誌,就看到同一串訊息序列。

點點看:全序廣播能拿來做什麼

全序廣播不是紙上談兵的抽象概念——它是好幾個重量級機制的地基。點下面每張卡片看它具體撐起了什麼。

🗄️ 資料庫複製
🔒 可序列化交易
🎫 鎖服務的柵欄令牌
🔁 與線性一致儲存互相實作
資料庫複製:如果每則訊息代表一次寫入,而每個副本都以相同順序處理相同的寫入,副本就會保持一致(除了暫時的複製延遲)。這個原理叫做狀態機複製(state machine replication)。
💎
一個深刻的洞見

線性一致的比較並設定(或遞增並取值)暫存器、全序廣播,三者其實都等價於共識——解開其中一個,就等於解開了其他兩個。單主複製其實就是把所有寫入丟給領導者、在單一 CPU 核心上排序;全序廣播要解決的,正是如何擴展吞吐量、以及怎麼處理領導者故障切換。

動手配配看:這幾種順序機制,各解決什麼問題?

把下面幾個機制,拖到它真正解決的問題上。配完按「對答案」。

Lamport 時間戳
版本向量
全序廣播
用一個精簡的 (計數器, 節點 ID) 配對,替所有操作排出一條「與因果一致」的全序
拖到這裡
需要分辨兩個操作到底是「並行」還是「有因果依賴」,不只是排出先後順序
拖到這裡
要保證帳號名稱唯一——不能事後才發現有人用更小的編號搶先,順序一旦傳遞就必須定案
拖到這裡

小試身手

因果、Lamport 時間戳、全序廣播——三個環環相扣的概念,來檢查一下有沒有串起來。

關於線性一致性與因果一致性,下列敘述何者最準確?
為什麼說全序廣播比單純的 Lamport 時間戳排序更強大?
🤝
下一站:共識

下一站:把順序講清楚之後,回到最現實的問題——多台機器要怎麼「一起」完成一個交易,或者更根本地,怎麼在誰都可能故障的情況下,對一件事達成共識?往下捲。

04

分散式交易與共識:全章總結

當一筆交易跨越好幾台機器,怎麼讓大家「說一不二」——兩階段提交的完美與破綻,以及容錯共識如何補上那個洞

單機很簡單,跨機器就麻煩了

在單一節點上,交易的原子性靠儲存引擎就能搞定:先把資料寫進磁碟,再附加一筆提交記錄——寫下那筆記錄的瞬間,就是決定提交或中止的關鍵時刻。是「單一裝置」讓提交成為原子動作。

但如果一筆交易得同時碰觸好幾個節點呢?各自獨立提交,看似省事,其實藏著地雷:有些節點可能偵測到衝突要中止,有些卻順利提交;有些提交請求在網路裡遺失、逾時中止,有些卻送達了;有些節點在寫完提交記錄前就當機回滾,有些卻成功了。

1
各自為政的下場

一旦有些節點提交、有些中止,它們就互相不一致了。

2
提交無法反悔

已提交的交易資料會被其他交易讀到、依賴——不能事後反悔。

3
唯一的鐵律

一個節點只有在確定所有其他節點也會提交時,才能提交。

🛡️
為什麼「不可反悔」這麼重要

交易一旦提交,資料就對其他交易可見,別人會開始依賴它——這正是「讀已提交」隔離等級的基礎。如果提交後還能反悔,所有依賴這筆資料的後續交易都得跟著回滾,代價會一路擴散下去。

直接讀原文:什麼是兩階段提交

先看書怎麼定義這個解決跨節點原子提交問題最常見的演算法——兩階段提交(2PC)

原文 · DDIA Ch.9 Two-phase commit is an algorithm for achieving atomic transaction commit across multiple nodes. 2PC uses a new component that does not normally appear in single-node transactions: a coordinator, also known as transaction manager. We call these database nodes participants in the transaction. When the application is ready to commit, the coordinator begins phase 1: it sends a prepare request to each of the nodes, asking them whether they are able to commit. If all participants reply "yes," the coordinator sends out a commit request in phase 2, and the commit actually takes place. If any participant replies "no," the coordinator sends an abort request to all nodes.
白話翻譯

兩階段提交,是一套讓跨多個節點的交易達成原子提交的演算法。

2PC 引入一個單節點交易沒有的新角色:協調者,又叫交易管理員。

我們把這些資料庫節點叫做這筆交易的「參與者」。

應用準備提交時,協調者啟動第一階段:向每個節點送出「準備好了嗎」的請求,問它們能不能提交。

如果所有參與者都回答「可以」,協調者就在第二階段送出真正的提交請求;只要有任何一個參與者說「不行」,協調者就對所有節點送出中止請求。

💒
2PC 像西方婚禮

牧師(協調者)先分別問新郎與新娘(參與者):「你願意嗎?」——這是準備階段。收到雙方的「我願意」後,牧師才宣布兩人結為夫妻——交易提交,昭告全場。說「我願意」之前你還能喊停;說出口之後,就不能反悔了。就算你說完「我願意」後昏倒、沒聽到牧師宣布,你還是結婚了——醒來後可以查詢全域交易 ID 問結果,或等牧師重試。

跑一遍 2PC:兩個「不歸點」與一個致命弱點

按「下一步」,看協調者如何指揮參與者完成一次提交——並看見那個讓 2PC 惡名昭彰的破綻:協調者一旦在關鍵時刻消失,會發生什麼事。

🧑‍⚖️
協調者 Coordinator
🗄️
參與者 A
🗄️
參與者 B
按「下一步」開始
🔒
懷疑狀態:卡在門口的新人

參與者投下「可以」之後、還沒收到最終結果,這個狀態叫懷疑狀態(in doubt)——就像你說完「我願意」,但牧師突然昏倒,沒人宣布結果,你只能站在原地等他醒來。真正的痛不是等待本身,而是等待期間鎖一直握著不放:其他交易碰不到那些列,甚至連讀都可能被擋,整個應用大片功能卡死,直到協調者恢復、或管理員手動介入。

協調者,其實是系統裡最脆弱的單點

書中有一句話點破 2PC 最根本的體質問題:

💡
協調者本身就是一種資料庫

它儲存著交易結果,必須像對待任何重要資料庫一樣謹慎對待——因為它一旦壞掉,後果不是「這筆交易失敗」,而是「所有卡在懷疑狀態的交易,鎖永遠握著」。

🎯
單點故障

許多協調者實作預設沒有高可用,它一掛,其他應用伺服器就卡在懷疑交易的鎖上動彈不得。

📦
破壞無狀態

協調者日誌成了關鍵持久狀態,原本無狀態、可以隨意重啟的應用伺服器,被迫變得有狀態。

📢
放大故障,而非容忍故障

2PC 要所有參與者都回應才能提交,任一環節壞掉交易就失敗——這跟「容錯」的目標恰恰相反。

👻
孤兒懷疑交易

實務上真的會發生:協調者日誌遺失或損毀,這些交易永遠卡著、握著鎖,唯一出路是管理員手動裁決,或動用 XA 的緊急逃生口「啟發式決策」——其實是「可能破壞原子性」的委婉說法。

共識登場:用「多數決」取代「單一協調者」

2PC 的痛點很清楚:它依賴一個不經選舉、單點故障的協調者,而且要求全體參與者同意。容錯共識(fault-tolerant consensus)演算法——像 Raft、Paxos 這一系——正是為了解決這個結構性弱點而生。

一致同意 + 完整性 + 有效性

沒有兩個節點決定不同的值、沒有節點決定兩次、決定的值必須是某節點提議過的——這三項是「安全性質」,就算大多數節點故障也始終維持,不會做出錯誤決定。

終止性

沒當機的節點最終都會決定某值——這是唯一的「活躍性質」,正式化了容錯:即使有節點故障,其他節點仍須做出決定,不能無限期卡住。

👥
只要多數還活著

共識只需要多數節點(quorum)正常運作即可繼續前進——要容忍 1 個故障,至少要 3 個節點;要容忍 2 個故障,至少要 5 個節點。

⚖️
共識 vs 2PC,關鍵差異就在這一句

共識的協調者(領導者)是選舉產生的,2PC 的協調者不是;共識只需要多數投票,2PC 要每一個參與者都說 yes。共識還有恢復程序——新領導者選出後,能讓節點回到一致狀態,不會像 2PC 那樣永遠卡死。這一切靠一個叫時代編號(epoch number)的機制保證:每個時代內,領導者唯一。

共識即服務:ZooKeeper 在真實系統裡扮演什麼角色

ZooKeeper、etcd 常被說成「分散式 key-value 儲存」,但它們的核心其實是把共識包裝成一個大家都能用的協調服務——用容錯的全序廣播,把少量、能放進記憶體的協調資料一致地複製到所有節點。作為應用開發者,你很少直接用它,而是透過 HBase、Kafka、Hadoop YARN 這類專案間接依賴它。

🏆 領導者選舉 Leader Election
🔐 分散式鎖 Distributed Lock
👥 成員管理 Membership Service
從多個實例選出一個當領導者,並在分區資源間分配節點。新節點加入或節點故障時重新分配——這正是需要共識的地方。
📋
它管的是「慢資料」,不是「熱資料」

ZooKeeper 通常只跑在 3 或 5 個固定節點上做多數投票,卻能服務大量客戶端——等於把協調工作「外包」給外部服務。它存的是慢速變動的資料,像「10.1.1.23 是分區 7 的領導者」,可能幾分鐘到幾小時才變一次;完全不適合存每秒變動上百萬次的應用執行狀態。

小試身手,然後回頭看看這三章走了多遠

先檢查兩題,再一起把第 7 到 9 章串成一條線。

兩階段提交(2PC)最惡名昭彰的弱點是什麼?
容錯共識演算法(如 Raft、Paxos)用什麼方式,解決了 2PC「協調者是單點故障」的弱點?
🧭
把第 7–9 章串起來看

第 7 章的交易,保護的是單機上的並行——多個操作同時發生,怎麼不互相踩到腳。第 8 章列出了分散式世界會出錯的所有方式:網路會延遲、會丟包、會分區;時鐘會偏移、會跳動;節點會停頓、會半死不活地假裝正常。第 9 章的答案是——即使如此,我們還是能讓多個節點對同一件事達成一致:2PC 是第一個嘗試,容錯共識則是更穩固的解法,而 ZooKeeper 這類協調服務,把共識包裝成了大家隨手可用的基礎設施。

📦
下一站:批次處理

第 7–9 章到此收尾:從單機交易的並行陷阱,到分散式系統的種種不可靠,再到共識如何在不可靠的基礎上立起可靠的保證。下一章,我們把視角切換到另一個世界——批次處理,看看整套資料系統除了「即時服務請求」之外,還怎麼消化海量資料。