01

為什麼要複製資料

不只是備份——把同一份資料放到很多台機器上,換來更快、更穩、更扛得住流量的系統

開場先聽一句忠告

第 5 章一開頭,作者沒有先講技術,而是引用了《銀河便車指南》作者 Douglas Adams 的一句話當題詞。這句話說的其實是「複製」這整章的潛台詞:你以為某個東西「不可能出錯」,恰恰是最危險的假設。

原文 · DDIA Ch.5 題詞 “The major difference between a thing that might go wrong and a thing that cannot possibly go wrong is that when a thing that cannot possibly go wrong goes wrong it usually turns out to be impossible to get at or repair.” — Douglas Adams, Mostly Harmless (1992) Replication means keeping a copy of the same data on multiple machines that are connected via a network. If the data that you're replicating does not change over time, then replication is easy. All of the difficulty in replication lies in handling changes to replicated data, and that's what this chapter is about.
白話翻譯

「『可能出錯的東西』和『不可能出錯的東西』,最大的差別在於:當那個『不可能出錯的東西』真的出錯時,你通常根本修不了它、碰都碰不到。」

——道格拉斯・亞當斯,出自《大致無害》(Mostly Harmless,1992)

「複製(Replication)」的意思是:把同一份資料的副本,放在透過網路相連的多台機器上。

如果要複製的資料永遠不會變,複製其實很簡單——存一次就搞定。

複製真正的難處,全部都出在「資料會變」這件事上——這也是整章要講的主題。

🎯
為什麼放這句題詞

「單一台機器」聽起來很簡單、很不容易出錯——直到它真的壞掉,你才發現整個服務也跟著停了。複製就是提早承認「機器會壞」,先把備援準備好。

複製,不只是「多印幾份」

你可能聽過「備份」,資料複製聽起來很像對吧?但備份通常是「存起來、平常用不到、出事才拿出來」;資料複製講的是另一件事——這些副本平常就同時在線上,隨時準備服務使用者。

每一台存著這份資料拷貝的機器,叫做一個副本(Replica)。想像成「影分身之術」——本體的資料被複製成好幾個一模一樣的分身,散布在不同機器上,各自都能獨當一面。

🥟
先給你一個畫面

放在抽屜裡、平常沒在跑的隨身硬碟備份,出事才拿出來救急——那是「備份」。而「複製」的副本,是隨時開著、隨時能接客人的分店,跟總店做一樣的事。這正是這一章要講的東西。

複製資料的三大好處

書裡明講:要複製資料,通常是為了下面這三個理由——不是因為「怕資料不見」這麼單純,而是要換來三種實實在在的好處。

🌍
降低延遲 Reduced Latency

把資料放在離使用者「地理上更近」的機器上,使用者存取時不用繞大半個地球,等待時間自然變短。

🛡️
提高可用性 Increased Availability

就算某一台機器掛了、故障、斷線,只要還有其他副本在,系統照樣能繼續運作,不會因為單點故障就整個停擺。

📈
提高讀取吞吐量 Increased Read Throughput

把能處理讀取請求的機器數量「橫向擴充」,多台一起分攤讀取流量,整體能撐住的讀取量就變大了。

🧋
一個畫面記住三件事

想像一間超紅的珍珠奶茶店:只開一間總店,你得跨縣市排隊(延遲高);總店突然公休你就沒奶茶喝(可用性低);排隊排到天荒地老(吞吐量低)。開好幾間分店,三個問題一次解決——這就是複製資料在做的事。

這一章先假設整份資料小到每台機器都放得下完整一份;之後遇到資料大到單機裝不下的情況,會用另一招「分區(partitioning)」來處理,那是後面章節的事,這裡先不展開。

好處是怎麼做到的?先看一眼骨架

要享受這三大好處,資料必須先能從一台機器,正確地傳到其他機器上。下面用一個最基本的骨架,帶你看一次「寫入」與「讀取」怎麼在多個副本之間跑動——這正是下一模組要細講的機制。

🙋
使用者
🗄️
機器 A
🗄️
副本 B
🗄️
副本 C
按「下一步」開始
🔁
看出關鍵了嗎

資料不是「一次複製完就沒事」——它是活的,一直在變。每次變更都要想辦法讓所有副本跟上。這件事「怎麼分工」、「誰能寫、誰只能讀」,正是下一模組要拆開來講的重點。

複製的兩難:資料一直在變,怎麼辦?

如果資料存進去之後永遠不會變,複製其實超簡單——每台機器存一次,就永久保持一致了。麻煩的是,真實世界的資料是活的:使用者一直在新增、修改、刪除——訂單、貼文、庫存數量,隨時都在動。

這就衍生出整章要處理的核心問題:資料一直在變,要怎麼讓多份副本保持一致性,並且快速同步到最新狀態?

⚠️
複製的兩難

資料一直在變,要怎麼讓多份副本同步?如果做得不好,使用者可能在不同副本上看到不一樣的答案——例如商品庫存有的機器顯示 100 件、有的已經是 99 件,導致超賣。這正是本章接下來要拆解的難題。

書裡接著介紹三種主流做法來解這個難題:單領導者複製多領導者複製,以及無領導者複製——幾乎所有分散式資料庫都是用這三種方式的其中一種。

小試身手

複製的動機搞懂了嗎?來兩題檢查一下:

小明說:「我每天把資料夾備份到一顆放抽屜裡的隨身硬碟,這樣就等於做了資料複製。」這個說法哪裡有問題?
新聞網站報導一則獨家新聞,瞬間湧入百萬人次同時點閱。網站靠把資料複製到十幾台伺服器、讓大家一起分攤讀取請求撐住了流量。這主要體現複製的哪個好處?
👑
下一站

複製的好處講完了,第一個、也是最常見的做法登場——指定一台機器當「老大」。

02

主從複製:最常見的複製方式

一個節點說了算、其他節點乖乖跟——從寫入、複製到「老大」掛掉之後怎麼辦

資料庫也需要一個「說了算」的人

如果一份資料同時存在好幾台機器上,馬上會冒出一個問題:每次寫入,到底要先寫哪一台?如果大家各寫各的,資料很快就會兜不起來。

最常見、也最簡單的解法,就是主從複製——選一個節點當「老大」,其他節點乖乖跟著複製,誰都不准自己作主。

先給你一個畫面

把資料庫想成一間咖啡店。首席咖啡師負責接下所有新訂單、做出咖啡,還把配方一步步記在筆記本上;學徒咖啡師不接新訂單,只負責照著筆記複製出一模一樣的咖啡,順便應付來問「我的咖啡好了嗎」的客人。

這裡有兩件事一定要記住:寫入,永遠只找老大(Leader)讀取,找老大或學徒都行(Follower)。這條規則簡單到有點反直覺——但正是它讓整套系統保持一致。

直接讀原文,旁邊就是白話

主從複製到底怎麼運作?原書用很精簡的三步驟講完,我們把關鍵幾句拉出來,原文在左、白話在右:

原文 · DDIA Ch.5 One of the replicas is designated the leader. When clients want to write to the database, they must send their requests to the leader, which first writes the new data to its local storage. Whenever the leader writes new data to its local storage, it also sends the data change to all of its followers as part of a replication log or change stream. Each follower takes the log from the leader and updates its local copy of the database accordingly, by applying all writes in the same order as they were processed on the leader. When a client wants to read from the database, it can query either the leader or any of the followers. However, writes are only accepted on the leader.
白話翻譯

其中一個副本會被指定為主節點。客戶端想寫入資料庫時,一定要把請求送給主節點,它會先把新資料寫進自己的本地儲存。

主節點每次把新資料寫進自己的儲存後,也會把這筆變更透過「複製日誌」或變更串流,發送給它所有的從節點。

每個從節點都會拿到主節點傳來的日誌,並依照主節點處理的「同樣順序」,一筆一筆套用,藉此更新自己那份資料副本。

客戶端想讀取資料時,可以查詢主節點,也可以查詢任何一個從節點。

但寫入這件事,只有主節點能接受。

🔑
整章最重要的一句話

「同樣順序」四個字才是關鍵——所有從節點都照著主節點決定的「變更順序」去套用,這正是讓一堆副本最終長成同一份資料的秘密,而不是靠誰算得快、誰運氣好。

拆開來看:老大跟學徒各自的工作

把上一節的原文拆成三件具體的事,你會發現主從複製其實沒有想像中複雜:

1
寫入只認一個入口

不管是新增使用者、發文還是改密碼,所有寫入請求都得先送到 Leader。它是資料的權威來源,也是唯一的「守門員」。

2
變更被記進一本流水帳

Leader 每寫一筆,就把這筆變化記進 複製日誌,像廚房裡的食譜筆記——後面的人照著做,就能重現一模一樣的結果。

3
學徒照單全收、分攤讀取

Follower 依序套用複製日誌,資料跟著同步,同時分擔掉大部分的讀取流量,讓 Leader 專心處理寫入。

📈
這樣設計,你多得到什麼

多數應用「讀多寫少」——首頁瀏覽遠比發文次數多。讓一群 Follower 分攤讀取,就是常說的讀取擴展(Read Scaling);把 Follower 擺在使用者附近,還能順便降低延遲。PostgreSQL、MySQL、MongoDB,甚至 Kafka 這類訊息佇列,都內建了這一套。

跑一遍完整的主從複製生命週期

從一筆寫入進來,到 Leader 突然故障、系統選出新老大——按「下一步」,看這齣戲怎麼演。

🙋
客戶端
👑
Leader(主節點)
🧑‍🍳
Follower A
🧑‍🍳
Follower B
按「下一步」開始這趟旅程
⚠️
故障轉移沒有你想的那麼順利

如果採用非同步複製,舊 Leader 故障前那些「還沒來得及複製給 Follower」的寫入,就可能永遠消失;萬一舊 Leader 後來又活過來、卻誤以為自己還是老大,就會撞出兩個 Leader 同時存在的腦裂(Split Brain)——這是自動故障轉移最怕遇到的地雷。

動手配配看:Leader 該等多久才回報成功?

Leader 寫完本地資料後,要不要等 Follower 確認收到,才對客戶端說「搞定了」?這是一個永恆的取捨題。把兩種模式拖到它對應的取捨後果:

同步複製
非同步複製
資料絕對安全:至少一個 Follower 確認收到才算成功,但寫入速度會變慢,Follower 掛掉時甚至會卡住整個寫入
拖到這裡
速度快、可用性高:Leader 寫完就立刻回報成功,但如果它在複製完成前故障,那筆最新寫入可能就永遠丟了
拖到這裡
回到咖啡店比喻

同步複製就像總咖啡師堅持等每個學徒都點頭確認記下食譜,才敢跟顧客說「咖啡好了」;複製延遲則是學徒還沒學會新配方前,顧客點到舊口味的那段尷尬時間差。多數系統會折衷用「半同步複製」——只逼一個關鍵學徒點頭,其他人慢慢跟上。

小試身手

主從複製的骨架抓到了嗎?來兩題檢查一下:

在主從複製架構中,使用者要「更新自己的電子郵件地址」該送去哪裡?其他人想「查詢」這筆資料又能從哪裡讀?
採用「非同步複製」的系統中,Leader 剛回報客戶端「寫入成功」就立刻故障了,這筆寫入最可能發生什麼事?
⏱️
下一站:時間差搞的鬼

複製日誌傳輸需要時間,這段「時間差」會讓使用者看到什麼奇怪現象?往下捲,我們去會會「複製延遲」這個麻煩鬼。

03

複製的陰影:最終一致性

領導者說「寫好了」的那一刻,追隨者可能還沒跟上——這段時間差,會讓使用者看到三種說不出哪裡怪的怪現象

資料為什麼會「遲到」?

上一模組講過,資料庫不只一份,而是有好幾份副本分散各地,這些副本叫 追隨者。 你寫入資料時(例如發一篇貼文),通常先送到 領導者, 由它接收、存好。

關鍵在於:領導者把變更「傳給追隨者」這件事,通常是 非同步 進行的。領導者不會傻傻等所有追隨者都更新完才跟你說「搞定」——它自己存好就立刻回你,然後在背景把變更慢慢傳出去。

這就像你點餐後,廚房(領導者)做好菜就先說「餐點準備中」,而不是等外送員(追隨者)把餐點送到你手上才通知你。

🛵
先給你一個畫面

領導者是廚房,追隨者是外送員。廚房出餐的瞬間跟你說「好了」,但外送員送到你手上還需要時間——這段等待,就是接下來要談的「複製延遲」。

複製延遲,與「最終」會一致

從領導者完成寫入,到追隨者實際套用完更新,中間這段時間差,叫 複製延遲。 在延遲期間,你若剛好讀到那個還沒跟上的追隨者,看到的就是舊資料。

但這個「不一致」不是永久的。只要你停止寫入、給點時間,所有追隨者最終都會追上領導者,資料變得完全一致——這就是 最終一致性。 複製延遲可能只有幾毫秒,也可能在系統負載重、網路不順時拉長到幾分鐘甚至更久。

💬
一句話記住它

「最終一致性」的意思是:資料總有一天會全部同步,但在那之前,你可能會看到舊的資料。「最終」這個詞,故意講得很模糊——沒人保證延遲的上限。

親眼看一次:剛發的留言,怎麼「消失」了

下面演給你看一個最尷尬的情境——你剛寫入一筆留言,馬上重新整理,卻打到一個還沒跟上的追隨者。按「下一步」跟著資料走一遍。

🧑
使用者(你)
👑
領導者 Leader
📀
追隨者 Follower
按「下一步」開始這趟旅程
😵
使用者不會管你「非同步複製」

對使用者來說,這就是「我的留言不見了、App 是不是壞了」。他們感受到的是怪現象,不是背後的技術原因——接下來就來認識三種最常見的怪現象。

原文現場:一段「通靈」對話

書裡用一段對話說明第三種怪現象——因果順序被打亂時,答案居然搶在問題之前出現。先看原文,再看白話。

原文 · DDIA Ch.5 Mr. Poons: "How far into the future can you see, Mrs. Cake?" Mrs. Cake: "About ten seconds usually, Mr. Poons." There is a causal dependency between those two sentences: Mrs. Cake heard Mr. Poons's question and answered it. To the observer it looks as though Mrs. Cake is answering the question before Mr. Poons has even asked it. Such psychic powers are impressive, but very confusing.
白話翻譯

波恩斯先生問:「凱克太太,你能看到多遠的未來?」

凱克太太答:「大概十秒吧,波恩斯先生。」

這兩句話之間有因果關係:凱克太太是先聽到問題,才回答的。

但旁觀者聽起來,卻像是凱克太太在問題被問出來之前就已經回答了。

這種「通靈能力」很厲害,但也超級讓人困惑。

原因很簡單:凱克太太的話走了延遲很短的追隨者,波恩斯先生的話卻走了延遲較長的那個。旁觀者先聽到「果」、後聽到「因」,邏輯整個亂套。

複製延遲的三種怪現象,與對應解方

怪現象各不相同,但都源自同一件事:不同的讀取,打到了進度不一的副本。每種怪現象都有一種「讀取保證」對症下藥:

1
讀己所寫 Read-Your-Writes

你剛發了貼文,馬上重新整理卻看不到——像是寫入「憑空消失」。解法:讓使用者讀「自己剛可能改過的資料」時,強制從領導者讀;其他人看,仍從追隨者讀,不犧牲效能。

2
單調讀取 Monotonic Reads

第一次刷新看到新留言,第二次刷新它卻「不見了」——像時間倒流。解法:用 使用者黏性, 把同一位使用者「黏」在同一個追隨者上,讀到的資料只會越來越新,不會忽新忽舊。

3
一致性前綴讀取 Consistent Prefix Reads

像凱克太太的「通靈」——答案先於問題出現,因果關係被打亂。解法:把有 因果關係 的寫入(例如一問一答),盡量路由到同一個領導者或同一個資料庫分區,順序在複製過程中就不會被打散。

🎯
三種保證,強度不同

這三種讀取保證都比「最終一致性」更強一點,但都還沒到「強一致性」那麼貴、那麼犧牲效能。它們是針對性地「補洞」——只補使用者真正會感到困惑的那幾個洞,其他讀取仍分散到追隨者上跑,兼顧體驗與效能。

動手配配看:怪現象該配哪種保證?

把左邊的讀取保證,拖到它該解決的情境上。

讀己所寫
單調讀取
一致性前綴讀取
使用者剛修改個人資料,重新整理卻看到修改前的舊版本
拖到這裡
同一個使用者連續刷新兩次頁面,第二次卻看到比第一次更舊的內容
拖到這裡
論壇上,讀者先看到某則「回覆」,過一會兒才看到它所回覆的「原貼文」
拖到這裡

小試身手

複製延遲的三種怪現象、三種解方,你抓到了嗎?來兩題檢查一下。

為什麼會出現「最終一致性」,導致使用者可能讀到舊資料?
要避免使用者連續刷新頁面時看到資料「時光倒流」(單調讀取問題),教材提出的策略是什麼?
🌐
下一站:如果不只一個老大呢?

主從複製只有一個「老大」可以寫,如果允許好幾個地方同時寫,甚至完全沒有老大呢?下一站,我們往下捲,看看多領導者與無領導者複製,會打開什麼樣的新問題。

04

多主與無主:更自由的複製模式

拿掉唯一的班長之後,寫入變快、變耐操,但也開始有人「打架」

班長不夠用了,怎麼辦?

上一站我們認識了單主複製:所有寫入都要先交給唯一的 領導者, 簡單、好懂,但也有代價——如果領導者在地球另一端,每次寫入都要跨海一趟;如果整個機房斷線,寫入就整個停擺。

這一站,我們把「唯一班長」這個限制拿掉,看看資料庫世界怎麼玩出更自由(但也更麻煩)的花樣: 多主複製 讓好幾個人同時當班長; 無主複製 則乾脆連班長都不要了。

📰
先給你一個畫面

把資料庫想成一家跨國新聞社。單主複製像是所有新聞都要先傳回總社審核發稿;多主複製像是每個分社自己就能發稿,之後再互相交換新聞;無主複製更徹底——沒有總社,每個記者手上都有一份完整的新聞檔案,大家直接互相核對、更新。

直接讀原文:衝突是怎麼冒出來的

我們挑書裡描述「寫入衝突」與「無主複製」登場的兩小段原文,一句對一句看白話。

原文 · DDIA Ch.5 The biggest problem with multi-leader replication is that write conflicts can occur, which means that conflict resolution is required. In a single-leader database, the second writer will either block and wait for the first write to complete, or abort the second write transaction, forcing the user to retry the write. On the other hand, in a multi-leader setup, both writes are successful, and the conflict is only detected asynchronously at some later point in time. Some data storage systems take a different approach, abandoning the concept of a leader and allowing any replica to directly accept writes from clients.
白話翻譯

多主複製最大的麻煩,就是寫入衝突可能發生——這代表你一定得處理「衝突要怎麼解決」。

單主資料庫裡,第二個寫入的人會被卡住等第一個寫完,或乾脆直接失敗、強迫使用者重試。但多主的情況是:兩邊的寫入都「成功」了,衝突要等到事後非同步同步時才會被發現。

有些資料儲存系統走另一條路:乾脆放棄「領導者」這個角色,讓任何一個副本都能直接接受客戶端的寫入。

💡
關鍵金句

單主複製會「當場」擋下衝突(卡住或直接拒絕);多主複製卻讓兩邊都「先成功再說」,事後才發現打架——這正是多主複製好用又危險的根源。

多個班長,換來三個好處

把「唯一領導者」換成「每個資料中心都有自己的領導者」,馬上換來三個實打實的好處:

效能飆升

客戶端把寫入送給「離自己最近」的領導者,不必千里迢迢跨海寫入,回應速度大幅提升。

更耐斷線

某個資料中心整個掛掉,其他資料中心的領導者依然能繼續接受寫入,不會整個系統停擺。

不怕網路抖動

多主複製通常靠非同步複製, 跨資料中心的連線暫時斷掉,本地寫入照樣正常,等網路恢復再補同步。

🍕
比喻:全球連鎖披薩店

每家分店都能自己接單、自己烤披薩(都是領導者)。台北顧客不用把訂單送到美國總部再繞回來;舊金山分店停電,台北、倫敦照樣營業;台北跟倫敦網路不穩,兩邊也還是能各自服務本地客人,事後再互相同步訂單。

三種拓樸,點點看:寫入怎麼走、誰會跟誰打架

把「班長制度」攤開來看,其實有三種完全不同的安排。點下面每個元件,看看在這種拓樸下,寫入路徑長什麼樣、又是誰可能跟誰起衝突。

🏛️ 單主複製 Single-Leader
🔀 多主複製 Multi-Leader
🌐 無主複製 Leaderless
點選上面任一種拓樸,看它的寫入路徑與衝突來源。
⚠️
共通的痛點

不管是多主還是無主,只要「不只一個地方能寫」,就一定要面對寫入衝突——差別只在於用什麼機制去發現它、解決它。

動手配配看:衝突發生後,該用哪招收拾?

多主複製最大的麻煩就是寫入衝突。書裡整理了幾種常見解法,把它們拖到最貼切的情境描述上。

最後寫入為準 LWW
合併雙方修改
交給應用層處理
自動合併的 CRDT 資料結構
系統只看每筆寫入的時間戳,直接留下時間最新的那筆,其餘悄悄丟棄——簡單但可能無聲無息弄丟資料
拖到這裡
兩人分別把「牛奶」「雞蛋」加進同一個購物車,系統把兩份清單接起來變成「牛奶、雞蛋」
拖到這裡
雲端文件偵測到衝突後,把兩個版本都保留下來,跳出視窗問你「要保留你的版本還是對方的版本?」
拖到這裡
計數器這種資料結構天生就知道怎麼把並行的加總操作組合起來,不管順序如何,最後結果都一致、不遺失
拖到這裡
🚨
LWW 的隱藏風險

最後寫入者獲勝(LWW) 聽起來公平,其實暗藏地雷:如果兩台裝置的時鐘沒對齊,「真正比較晚做的修改」反而可能因為時間戳比較舊而被覆蓋掉——使用者根本不會知道自己的修改憑空消失了。

沒有班長,怎麼知道大家有沒有更新到位?

無主複製乾脆不設領導者,任何副本都能直接接受寫入。但這樣要怎麼確保讀到的是最新資料?答案是靠一組數字說話:

🗂️
n:總副本數

這筆資料總共存了幾份。

✍️
w:寫入法定人數

要幾個副本回報「我寫好了」,才算寫入成功。

📖
r:讀取法定人數

要問幾個副本,才拿去比對版本、回傳最新的那份。

w + r > n時,有個很神奇的保證:任何一次成功的寫入,跟任何一次讀取,它們各自涉及的副本集合「一定會有重疊」——所以你讀到的裡面,至少有一份是包含最新寫入的。

📖
比喻:社區共享食譜書

社區裡每個人手上都有一本食譜書(副本)。你新增一道菜時,不必等全部人都確認,只要 w 個朋友回報「我抄進去了」就算發佈成功;查食譜時你問 r 個朋友,比較誰的版本比較新。只要「發佈的那群人」和「查詢的那群人」加起來一定會有交集(w + r > n),你就總能問到擁有最新食譜的人。要是問到拿舊版本的朋友,你還會順手更新給他——這就是 讀取修復

副本暫時不在家,寫入就失敗嗎?

如果某個副本剛好斷線或當機,無主系統不會因此拒絕寫入。它靠兩個機制撐住可用性:

寬鬆法定人數

原本該收資料的副本連不上,就先找「附近可用」的鄰居代收,只要湊滿 w 個確認,一樣算寫入成功。

提示性移交

等原本離線的副本恢復上線,剛剛代收資料的鄰居會把資料連同「這是要給誰的」提示一起交還回去。

📦
比喻:包裹代收服務

物流大哥要把包裹送到某個圖書角,結果它鐵門深鎖。物流大哥就先交給隔壁有開門的鄰居代收,並悄悄留言「這是給隔壁的,它開門了幫我轉交」。等原本那間重新開門,鄰居就把包裹還過去——寫入不會因為一個節點暫時不在家就失敗。

小試身手

這一站的內容不少,來兩題檢查一下有沒有抓對重點:

某電商用多主複製,台北與倫敦的領導者幾乎同時把同一件商品的庫存都改成「剩 1 個」。這最可能導致什麼問題?
無主複製中,設定 w + r > n(n 為總副本數)主要保證了什麼?
⏱️
下一站:誰先誰後?

多主、無主都可能讓兩筆寫入「同時」發生,要怎麼判斷誰先誰後、甚至兩者根本互不相干?往下捲,我們要正式拆解「並發」這件事。

05

版本向量:偵測與解決衝突

沒有班長盯場的時候,系統怎麼知道兩筆寫入「互不知情」,又怎麼把它們都留住?

誰先誰後,不能只看時間戳

無領導者複製的世界裡,同一筆資料可能同時被好幾個客戶端修改。如果兩筆修改各自獨立、互不干涉,那沒問題;但如果兩邊剛好動到同一個地方,麻煩就來了——這就是 平行寫入

你可能直覺會想:「比對時間戳,誰的時間晚就聽誰的」。這正是 最後寫入為準(LWW) 的邏輯,但這招在分散式系統裡很危險——不同機器的時鐘很難完全同步,「誰的時間比較新」常常靠不住。更糟的是,就算時間戳準確,LWW 也只是粗暴覆蓋,完全不管那筆被蓋掉的寫入裡,可能藏著別人還沒看到、也還沒被合併進來的重要修改。

⚠️
LWW 的代價:資料悄悄消失

兩位廚師同時喊出不同的烤箱溫度,系統只認「最後聽到的那個」——另一個溫度就這樣不留痕跡地被丟掉了。你不會看到任何錯誤訊息,資料就是安靜地不見了,這才是 LWW 最可怕的地方。

判斷順序的關鍵不是時間,是「誰依賴了誰」

那該怎麼判斷兩筆寫入到底是「有先後」還是「平行」?書裡給出的定義非常精準,我們直接讀原文,旁邊配白話。

原文 · DDIA Ch.5 An operation A happens before another operation B if B knows about A, or depends on A, or builds upon A in some way. We can simply say that two operations are concurrent if neither happens before the other (i.e., neither knows about the other). For defining concurrency, exact time doesn't matter: we simply call two operations concurrent if they are both unaware of each other, regardless of the physical time at which they occurred. If one operation happened before another, the later operation should overwrite the earlier operation, but if the operations are concurrent, we have a conflict that needs to be resolved.
白話翻譯

如果操作 B 知道操作 A、依賴 A,或是建立在 A 的結果上,我們就說「A 發生在 B 之前」。

簡單說:如果兩個操作互相都不知道對方,也就是誰都沒「發生在」誰之前,那它們就是平行的。

判斷平行與否,重點根本不是物理時間——只要兩邊彼此不知情,不管它們實際上隔了多久,都算平行。

如果一個操作發生在另一個之前,後面那個直接覆蓋前面的就好;但如果兩者是平行的,就出現了需要解決的衝突。

💡
「發生在前」不是時鐘的事,是因果的事

你先發了一篇貼文,朋友看到後留言——留言「知道」貼文的存在,所以貼文發生在前。但如果你和朋友在完全不相關的社群平台上,同時各自發了一篇貼文,彼此都不知道對方在做什麼,那這兩篇貼文就是平行的——沒有誰先誰後,只有「互不知情」。

拆解一次真實的平行寫入:兩支手機、同一台購物車

光說「平行」有點抽象,我們直接跑一次書裡最經典的例子——你和朋友同時打開同一個購物車 App,其中一支手機還離線中。按「下一步」看資料庫怎麼一步步發現這兩筆寫入互不知情,最後又怎麼把它們合併起來,而不是隨便丟掉一筆。

📴
手機 A(曾離線・加鞋子)
📱
手機 B(在線・加帽子)
🗄️
資料庫節點
🧭
版本向量比對
🛒
合併後的購物車
按「下一步」開始這趟平行寫入之旅
🧭
版本向量到底在記什麼?

版本向量不是一個數字,而是「每個副本各自改到第幾版」的一張清單,例如 {手機A: 1, 手機B: 1}。寫入時把你讀到的版本號一起帶回去,資料庫就能比對:你的修改「知道」多少歷史。如果知道的比資料庫現在少,那就是平行寫入,不能直接覆蓋。

平行寫入之後:同級版本、合併、墓碑

偵測到平行寫入只是第一步,資料庫接下來要做的不是「選一個」,而是「都留著,交給應用程式處理」。這中間有三個關鍵動作:

1
保留為同級版本(Siblings)

帽子和鞋子彼此不知情,資料庫不會二選一,而是把兩筆都存下來,變成同級版本——兩份「都有效」的草稿。

2
應用程式負責合併

資料庫把同級版本一起回傳,剩下的合併工作交給應用程式:像購物車這種只會累加的情境,最簡單的合併方式就是取聯集——帽子、鞋子都留著。

3
刪除要留下墓碑

但如果購物車也能刪除商品,單純取聯集會讓已刪除的商品「復活」。所以刪除不能真的抹掉,而要留一個墓碑標記——合併時看到墓碑,就知道這項目該被視為已刪除,即使另一個版本裡還留著它。

🍳
生活比喻:多人共編食譜

想像你和朋友一起在數位白板上編同一份食譜。你加了「麵粉」、朋友同時加了「雞蛋」,兩人互不知道對方動了手——這就是平行寫入,白板會把兩份修改都留著,等你們一起討論出最終食材清單。如果你決定「這道菜不加鹽」,白板不會直接擦掉,而是留一張「鹽(已刪除)」的小紙條,免得合併時鹽又莫名其妙地跑回來。

實戰協定:讀取要拿版本,寫入要還版本

要讓上面這套機制運作,應用程式跟資料庫之間有一個簡單但關鍵的約定——讀的時候順手拿走版本資訊,寫的時候一定要帶回去。

📥
讀取時拿到版本

應用程式讀資料,資料庫連同目前的版本號(或版本向量)一起回傳,就像拿到一份「修改歷史紀錄」。

📤
寫入時還回版本

修改完要存回去時,必須把「剛才讀到的版本」一起送回,等於告訴資料庫:「我這次修改是基於這個舊狀態做的」。

🧩
資料庫比對再決定

帶回的版本如果被資料庫現有版本完全涵蓋,就安全覆蓋;如果彼此互不涵蓋,就是平行寫入,產生新的同級版本。

🦸
實用超能力:自動合併的 CRDTs

在應用程式碼裡手動合併同級版本又累又容易出錯,所以有一類特殊資料結構叫 CRDTs(無衝突可複製資料類型), 專門把集合、計數器這類資料的合併自動化,連刪除的墓碑語意都內建處理好,很適合購物車、協作編輯這種天生會有大量平行修改的場景。

小試身手

版本向量、同級版本、墓碑——這三個概念環環相扣。來兩題檢查一下:

判斷「操作 A 發生在操作 B 之前」,關鍵依據是什麼?
合併多個同級版本時,如果某項目在其中一個版本已被刪除、但另一個版本還留著它,為什麼需要用「墓碑」而不是直接刪除?
🎬
下一站

單主、多主、無主,外加各種一致性保證,到底該怎麼選?整章收束一次講清楚。

06

大局:複製模式選擇地圖

單主、多主、無主——三條路都通往「多台機器同一份資料」,差別在你願意犧牲什麼

先倒帶:這一章到底在解決什麼問題?

這一章從頭到尾都在回答同一個問題:把同一份資料放到複製(replication)到好幾台機器上,聽起來很單純,一旦資料會「變動」,就立刻變成一道難題——變動要怎麼有秩序地傳播出去?

你已經跟著我們走過三種做法:只有一個節點能寫的單主複製、允許好幾個節點都能寫的多主複製、以及完全沒有固定領導者的無主複製。這一站不再逐節細講,而是把它們攤開放在同一張地圖上,讓你看清楚——選哪一種,其實是在回答「我最不能忍受哪種犧牲」。

🍳
同一本食譜祕笈,三種管理方式

還記得烹飪俱樂部的比喻嗎?單主是「總主廚一人說了算」;多主是「各地分區主廚各自開發特色菜,定期交換心得」;無主是「一群美食部落客互相打聽,問夠多人就能拼出最新版本」。同一本食譜,三種完全不同的治理方式。

直接讀原文:作者怎麼定義這整章

這段話出現在本章一開頭,等於是作者親口寫下的「本章路線圖」——先講複製是什麼,再列出接下來要教的三條路。左邊原文、右邊白話,一句對一句。

原文 · DDIA Ch.5 Replication means keeping a copy of the same data on multiple machines that are connected via a network. If the data that you're replicating does not change over time, then replication is easy: you just need to copy the data to every node once, and you're done. All of the difficulty in replication lies in handling changes to replicated data, and that's what this chapter is about. We will discuss three popular algorithms for replicating changes between nodes: single-leader, multi-leader, and leaderless replication. Almost all distributed databases use one of these three approaches.
白話翻譯

複製,意思是把同一份資料,透過網路,保存在好幾台機器上。

如果你要複製的資料永遠不會變,那複製很簡單——每台機器複製一次就完工了。

複製真正的難處,全都出在「資料會變動」之後怎麼處理,而這正是這一章要講的。

我們會講三種主流做法:單主複製、多主複製、無主複製。

幾乎所有分散式資料庫,用的都是這三種方式之一。

🔑
記住這句就等於記住整章

「複製的難處全在資料會變動之後」——資料不變,複製是體力活;資料一直變,複製才變成需要設計的問題。三種模式,就是三種不同的「誰有權決定變動順序」的答案。

三條路,三種脾氣

快速複習一輪,每種模式一張卡:它怎麼運作、換來什麼、犧牲什麼。

👑
單主複製

一個領導者收所有寫入,其餘追隨者照順序複製、負責讀取。架構最簡單、資料最一致,但領導者是單點瓶頸,故障轉移期間可能有停頓或短暫資料遺失。

🌍
多主複製

好幾個節點都能收寫入,彼此再非同步同步。換來「就近寫入」的低延遲、和「一地斷線不影響其他地」的高可用,但代價是要正面處理寫入衝突

🕸️
無主複製

沒有領導者,寫入直接送給多個節點、讀取也問多個節點比對版本,靠法定人數(quorum)機制撐住新鮮度。韌性最強、服務最難被打斷,但只保證最終一致性,讀到的可能不是最新版。

⚖️
沒有「最好」,只有「最適合」

三種模式的關鍵差異,其實是同一道拉鋸反覆出現:想要更強的一致性,就要犧牲一點可用性或延遲;想要更高的可用性,就要接受資料短暫不同步。選型的第一步,是先誠實回答「我最怕哪種代價」。

點點看:每種模式的「一致性防線」擺在哪?

三種模式其實是把「誰先看到最新資料」這件事,安排在流程的不同位置。點下面每個角色,看它在各模式中扮演的防線。

👑 領導者(單主)
🌍 多個領導者(多主)
🕸️ 法定人數(無主)
🩹 讀修復 / 反熵
點擊上面任一元件,看它在不同複製模式裡負責守住什麼。

動手選型:把應用情境拖到對的複製模式

這是本章壓軸的整合練習。下面四種常見的應用情境,各自最適合哪種複製模式(或它的變形)?拖拖看,拖完按「對答案」。

單主同步
多主+衝突解決
無主+版本向量
單主非同步
單一資料中心內的金融交易系統,寫入絕對不能遺失,能接受偶爾的寫入延遲
拖到這裡
全球多地團隊要能同時編輯同一份協作文件,各地都要能就近寫入
拖到這裡
離線優先、網路時有時無的行動裝置 app,斷線也要能繼續讀寫
拖到這裡
單純想擴展讀取效能的新聞網站,寫入量不大、讀取量爆量,架構越簡單越好
拖到這裡
🧭
怎麼想出這張對應表?

先問「寫入能不能容忍多個入口」——不能,就走單主;能,且是為了跨地低延遲或離線可用,就走多主或無主。再問「一致性要多硬」——金融交易這種絕不能丟資料的,寧可犧牲一點延遲也要同步確認;單純讀多寫少的網站,非同步單主的簡單架構就夠了。

整章總複習

來兩題貫穿全章的題目,檢查你是不是真的把三種模式串起來了。

單主複製之所以「幾乎不會有寫入衝突、資料一致性最容易理解」,是用什麼代價換來的?
「版本向量」這類用來判斷「哪些修改是並行發生、彼此衝突」的機制,為什麼在無主複製裡特別重要,但單主複製幾乎用不到?
💾
下一站(其他章)

這一章解決的是「一份資料,複製到多台機器」——為了容錯、就近服務、分攤讀取。但如果資料量大到一台機器根本裝不下呢?下一站要處理的,就是把資料「切開」分裝到不同機器上的難題。