
如同先前的故事中提到的,在分散式架構(Distributed Systems)中,隧道坍方(分區,Partition)是必然會發生的,當台北分店與台中分店因隧道坍方而暫時失去聯繫時,鼴鼠經理可能會選擇「可用性優先(AP,Availability/Partition Tolerance)」模式,而繼續營業。本篇文章將探討在網路連線恢復後,資料庫如何維持最終資料的一致性(Consistency)。
一、衝突的起源:併發寫入 (Concurrent Writes)
在分散式系統中,如果兩個節點在未溝通的情況下對同一個目標(標識符,Identifier)進行修改,便會發生衝突。
- 場景:ID 為
001的最後一條「限量黃金蚯蚓」庫存剩餘 1。此時隧道發生坍方,台北分店與台中分店都選擇繼續營業,且幾乎同時地賣出了一條「限量黃金蚯蚓」給客人。在隧道修好後,總部的庫存系統同時接收到兩家分店的訂單,而這最後一條「限量黃金蚯蚓」應該交給哪一家分店? - 結果:若無鎖定機制,兩家分店都會認為自己成功賣出了最後一件商品。
二、 簡易調解法:最後寫入者勝 (Last Write Wins,LWW)
- 最後寫入者勝 (Last Write Wins,LWW):如同字面的意思,每筆資料修改請求都會有它的時間戳記,資料庫在接收到衝突的請求時,可以根據時間戳記來判斷,讓最新的寫入操作蓋過前一個寫入操作,是一種簡單粗暴的解決方案。
- 時間的假象:然而,在「全球」等級的分散式系統中,各地的伺服器的 NTP(Network Time Protocol)同步存在有誤差(通常在毫秒級別),難以完全精確,這稱作「時鐘偏移 (Clock Skew) 」問題。在極高併發的情況下,這幾毫秒的偏誤會導致邏輯上的順序顛倒。
- 優點與缺點:優點,簡單且快速,不需要複雜的系統來處理資料衝突的問題。缺點,「努力會憑空消失」。以文章開頭提到的最後一條「限量黃金蚯蚓」為例,假設台中分店的訂單是最後寫入的,系統會根據時間戳記將最後一條「限量黃金蚯蚓」交給台中分店,而台北分店的客人已經付了錢但訂單卻憑空消失,將引發嚴重的公關危機!
- LWW的致命缺陷 -「狀態覆蓋」導致的「增量遺失」:
- LWW 的邏輯(狀態寫入),以「普通蚯蚓」的庫存計算為例:
- 台北分店讀取當前蚯蚓庫存:100。
- 台北分店計算蚯蚓庫存:100 + (-20) = 80。
- 台北分店發送訂單請求:
SET sku = 80(時間戳 T1)。 - 台中分店讀取當前蚯蚓庫存:100(此時台北分店的更新尚未同步)。
- 台中分店計算蚯蚓庫存:100 + (-50) = 50 (時間戳 T2)。
- 台中分店發送訂單請求:
SET sku = 50(時間戳 T2)。
- 結果:如果 T2 > T1,最終的蚯蚓庫存將被修改為 50,台北分店賣出 20 條蚯蚓的紀錄在系統中憑空消失了,正確結果應為 100 + (-20) + (-50) = 30。會出現這樣的錯誤是因為,LWW 處理的是「賦值(Assignment)」,而庫存數量計算需要的是具備交換律的「加法(Addition)」。
- 具體來說,由於賦值操作不具備交換律( A ⭢ B 不同於 B ⭢ A),假設初始庫存是 100, 先執行
SET 80(台北店),再執行SET 50(台中店),最後結果 = 50; 先執行SET 50(台中店),再執行SET 80(台北店),最後結果 = 80。順序一換,結果就完全不同。
- 具體來說,由於賦值操作不具備交換律( A ⭢ B 不同於 B ⭢ A),假設初始庫存是 100, 先執行
三、 進階調解術:鼴鼠加法計數器 - 無衝突複製資料類型(Conflict-free Replicated Data Types,CRDTs)
- CRDTs 之所以被稱為「魔法」,是因為它在數學上保證了:只要所有副本收到的資訊相同,最後的結果必定一模一樣,完全不需要一個中央節點來當裁判。為了達成「最終一致性」,CRDTs 在設計上必須滿足三個數學特性(這就是它的魔法陣):
- 結合律 (Associative):(a + b) + c = a + (b + c)(分組順序不影響結果)。
- 交換律 (Commutative):a + b = b + a(到達順序不影響結果)。
- 冪等律 (Idempotent):a + a = a(重複收到同樣的資訊,結果不變)。
並且,為了實現這個魔法,鼴鼠經理有兩套「傳輸戰術」與兩套「資料結構」
1. 兩大傳輸戰術:操作派 vs. 狀態派,這決定了分店之間如何「對帳」。
- 基於操作 (Operation-based)的CmRDT (Commutative Replicated Data Types):
- 戰術:只傳送「操作指令」(如:台北店 -10 條蚯蚓)。
- 優點:傳輸量極小,更新速度極快。
- 缺點:對隧道要求很高,需要能夠即時傳輸資料,不能掉包,且指令順序必須正確(或具備交換律)。
- 基於狀態 (State-based)的CvRDT (Convergent Replicated Data Types):
- 戰術:直接傳送「整份最新帳本」(如:台北店現在總共 50 條)。
- 優點:極度耐操。即使隧道不穩、重複收到同樣的帳本,雙方只要取「聯集」就能得到正確結果。
- 缺點:資料傳輸量較大。
2. 兩大資料結構:數值派 vs. 內容派,這決定了資料如何「合併」。
- 數值型資料(加法運算 - Counters):
- 場景:經驗值、按讚數、剩餘庫存。
- 設計:不需要爭奪同一個資料格,而是每個分店維護自己的資料小格子,互不干擾。
- 實作:台北分店在帳本的台北分店小格子記下
-20,台中分店在帳本的台中分店小格子記下-50,合併時總庫存就會是 100 + (-20) + (-50) = 30。這下即便網路延遲讓台北的指令晚到,台北分店賣出 20 條蚯蚓的紀錄也不會消失。
- 內容型資料(排序聯集 - Sets/Lists):
- 場景:留言板、購物車清單。
- 設計:採用 Append-only (僅追加模式),每次更新不會只存一個「數字」,而是存一個「操作日誌(Log)」。
- 實作:蚯蚓漢堡總部舉辦創意菜單競賽,各分店都想推出自己的獨門漢堡配方,台北分店在本地日誌新增一筆
[14:00 新增:翠綠蚯蚓漢堡],且台中分店在本地日誌新增一筆[14:05 新增:赤紅蚯蚓漢堡]。當隧道修好進行合併時,系統不進行覆蓋,而是將兩店的日誌取「聯集」並根據時間戳記排序,最終兩間分店的清單都會同步為[翠綠蚯蚓漢堡 (14:00), 赤紅蚯蚓漢堡 (14:05)],確保兩份新增的創意品項都同時存在。
- CRDTs 的限制:
- 限量商品怎麼辦? 回到文章開頭的「限量黃金蚯蚓」,場景:只剩最後一條,兩家分店都賣出去了, CRDTs Counter 能解決嗎?讓我們試試用 CRDTs 的「分店小格子」加法來試試看,帳本的「限量黃金蚯蚓」庫存為 1 ,台北分店在台北分店小格子記下
-1,而台中分店在帳本的台中分店小格子記下-1,合併後 1 + (-1) + (-1) = -1 (負數!) - CRDTs 確實不會讓訂單「憑空消失」,但它也只能誠實地告訴你:庫存不夠,超賣了! 系統會記錄「這兩筆訂單衝突了」,但無法自動決定給誰。
- 最佳解法:預防勝於治療,對於「只剩一個」的限量商品,最好的策略是: 使用 PC/EC 模式(請參考 PACELC 定理),隧道斷了就「拒絕接單」,而不是硬著頭皮繼續賣,只等隧道恢復後才開放購買。
- 限量商品怎麼辦? 回到文章開頭的「限量黃金蚯蚓」,場景:只剩最後一條,兩家分店都賣出去了, CRDTs Counter 能解決嗎?讓我們試試用 CRDTs 的「分店小格子」加法來試試看,帳本的「限量黃金蚯蚓」庫存為 1 ,台北分店在台北分店小格子記下
四、 結語:鼴鼠經理的衝突管理術
在學習了神祕的 CRDTs 魔法後,鼴鼠經理悟出了經營一家全球等級的連鎖店的真理:「資料的本質,決定了調解的方法。」
在分散式系統中,沒有任何一套計算方法或資料結構能夠解決所有的衝突,一名優秀的鼴鼠經理,必須先看透資料的靈魂:
- 當資料是「狀態」時(如:名字、開關):我們追求最新,可以使用 LWW 或多版本控制(Multiversion concurrency control,MVCC)。
- 當資料是「數值」時(如:存款、庫存數量):我們追求累加,使用 CRDT 的「分店小格子」加法,保證每一分努力都被計入。
- 當資料是「內容」時(如:留言、清單):我們追求並存,使用 Append-only 的日誌與時間戳記來排序,讓每一分創意都能被記錄。
在充滿不確定性的地底隧道中,隧道坍方(Partition)和衝突(Conflict)從來就不可怕,它是系統規模化後的必然產物。真正可怕的,是我們在設計系統時,因為貪圖方便選擇了簡單的「覆蓋」,而隨意地將遠方分店戰友們的努力與心血抹除。




















