系統設計 · 基礎手冊

資料複寫基礎概念:高可用與一致性的權衡

資料複寫(Data Replication)是分散式系統中保證高可用、低延遲與可擴充性的核心基石。然而,當數據被複製到多個節點時,我們必須面對物理極限——網路延遲與節點失效。這份指南將帶你理解複寫拓撲、同步與異步的權衡,以及在複製延遲下常見的一致性異常與解決方案。

Replication Single-Leader Multi-Leader Leaderless Replication Lag 面試導向
01

總覽與定位

為什麼需要複寫?它與分片有何不同?在系統設計版圖中的座標為何?

在分散式系統中,我們幾乎不可能只將資料儲存在單一伺服器上。單點意味著單點故障(SPOF)、地理延遲限制以及吞吐量的瓶頸。為了解決這些問題,我們需要將相同的資料複製並儲存在多個透過網路連接的節點上,這就是資料複寫(Data Replication)。

複寫的三大核心動機

  • 高可用性 (High Availability, HA):當部分節點宕機、斷網或發生硬體故障時,系統仍能繼續運作,提供不間斷的服務。
  • 降低延遲 (Low Latency):將資料儲存在地理位置上更接近用戶的節點(如 CDN 或多區域資料庫),縮短資料傳輸的物理距離。
  • 提升吞吐量 (Scalability/Read Throughput):透過讀寫分離,讓多個副本(Replica)共同分擔讀取請求,極大地提升系統的併發讀取能力。
座標複寫 (Replication) vs. 分片 (Partitioning/Sharding)

這是面試中極易混淆的概念:
• 複寫 (Replication):將相同的資料複製到多個節點。目的是容錯與擴充讀。
• 分片 (Partitioning/Sharding):將不同的資料子集拆分到不同節點。目的是擴充寫入與儲存容量。
在實際的大規模系統(如 Cassandra, Spanner)中,兩者通常是結合使用的:每個分片本身都會有多個複寫副本。

一句話答法資料複寫是將相同資料拷貝到多個節點以換取高可用與低延遲的機制;它與分片(拆分資料以擴充容量)相輔相成,是分散式資料庫的底層基石。

02

複寫拓撲:單主、多主與無主

決定「誰能接受寫入、誰負責傳播更新」的架構藍圖。

分散式資料庫如何組織節點間的寫入與讀取關係?這取決於系統採用的複寫拓撲(Replication Topologies)。主流拓撲可分為以下三種:

1. 單主複製 (Single-Leader / Master-Slave)

這是最經典且應用最廣泛的模式(如 MySQL, PostgreSQL, Redis)。
• 運作機制:系統中只有一個節點被指定為 Leader (Master),所有寫入請求必須發送給 Leader。Leader 將更新寫入本地後,將變更日誌(Replication Log / Change Stream)發送給所有的 Follower (Slave/Replica) 節點。讀取請求則可以路由到 Leader 或任何 Follower。
• 失效轉移 (Failover):若 Leader 宕機,系統會透過心跳檢測(Heartbeat)發現,並啟動選舉機制,將其中一個 Follower 提升(Promote)為新 Leader。

陷阱腦裂異常 (Split-brain)

在 Failover 過程中,若原 Leader 因為網路分區(Network Partition)只是暫時與叢集失去聯繫,但並未宕機,此時叢集選舉出了新 Leader。當網路恢復時,系統中會同時存在兩個自認為是 Leader 的節點,它們同時接受寫入,導致資料嚴重衝突。解決方案通常需要引入租約機制(Lease)或基於多數決(Quorum)的防禦令牌(Fencing Token)。

2. 多主複製 (Multi-Leader / Multi-Master)

當系統跨越複數個地理區域(Datacenter)時,單一 Leader 會面臨跨洋寫入的巨大延遲。此時可採用多主複製。
• 運作機制:在每個資料中心都設有一個 Leader。本地的寫入請求直接發送給該資料中心的 Leader,各資料中心的 Leader 之間再異步地將變更同步給彼此。
• 衝突解決 (Conflict Resolution):多主複製最大的挑戰是寫入衝突(兩個資料中心同時修改同一筆資料)。常見的解決策略包括:
1. 最後寫入者勝 (Last-Write-Wins, LWW):基於實體時間戳(Timestamp),但實體時鐘存在漂移風險。
2. 無衝突複製資料類型 (CRDT):利用數學結構(如 Grow-only Counter)在併發寫入時自動合併,避免衝突。
3. 自訂衝突解決器:在應用層編寫合併邏輯。

3. 無主複製 (Leaderless / Dynamo-style)

由 Amazon Dynamo 帶起風潮的去中心化架構(如 Cassandra, Riak)。
• 運作機制:系統中沒有 Leader 的概念。Client 直接將寫入併發發送給多個節點(通常是 N 個副本)。讀取時,Client 也會併發向多個節點發送讀取請求,並透過版本號(Version Vector)來識別並解決可能存在的數據不一致。
• Quorum 讀寫機制:為了保證讀取到最新寫入的數據,寫入成功的節點數 W 與讀取請求的節點數 R 必須滿足以下公式:

W + R > N

當此條件成立時,寫入節點集合與讀取節點集合必定存在交集,因此讀取操作一定能拿到至少一個最新寫入的副本。

維度 / 拓撲 單主複製 (Single-Leader) 多主複製 (Multi-Leader) 無主複製 (Leaderless)
寫入路徑 僅限單一 Leader 節點 各區域的 Leader 節點 併發寫入任意 N 個節點
讀取路徑 Leader 或任意 Follower 本地資料中心的任意節點 併發讀取 R 個節點並取最新值
寫入衝突 無(Leader 負責序列化寫入) 極高,需複雜的衝突解決策略 高,需透過 Quorum 與 Versioning 解決
適用場景 絕大多數傳統 RDBMS 應用 跨國多活資料中心、離線協作編輯 高寫入吞吐、高可用、無單點故障要求

拓撲對比:單主複製結構最簡單,一致性最容易維護;無主複製則將複雜度移至 Client,以換取極高的寫入可用性。

一句話答法單主複製由單一節點序列化所有寫入,結構簡單但有單點寫入瓶頸;多主複製解決跨區域延遲但引入衝突難題;無主複製靠 Quorum 讀寫保證一致性,具備極高可用性。

03

同步 vs. 異步複寫

在「數據持久性 (Durability)」與「寫入延遲 (Latency)」之間的終極折衷。

當 Leader 收到寫入請求時,它必須決定何時向 Client 返回成功。這涉及到同步與異步的權衡:

同步複寫 (Synchronous Replication)

Leader 收到寫入請求後,將更新發送給 Follower,並阻塞等待 Follower 完成寫入並回傳確認(ACK),最後才向 Client 返回成功。

  • 優點:強持久性。保證 Follower 上的數據與 Leader 完全一致。即使 Leader 瞬間宕機,數據也絕不丟失。
  • 缺點:寫入延遲極高(取決於最慢的 Follower 網路 RTT);可用性極差,任一 Follower 故障或網路抖動,都會導致整個系統的寫入操作被阻塞。

異步複寫 (Asynchronous Replication)

Leader 寫入本地後,立刻向 Client 返回成功,並在背景(Background)異步地將更新發送給 Follower。

  • 優點:極低的寫入延遲,高寫入可用性。即使所有 Follower 都斷線,Leader 仍能繼續接受寫入。
  • 缺點:存在數據丟失風險。若 Leader 在變更尚未同步到 Follower 前宕機,這部分未同步的寫入將永久丟失。此外,這會帶來 Replication Lag,導致讀寫不一致。

折衷方案:半同步複寫 (Semi-synchronous)

為了兼顧兩者的優點,現代資料庫(如 MySQL Semi-Sync)常採用半同步架構:Leader 僅等待其中一個 Follower 確認(通常是專門的 Sync Follower)即返回成功,其餘 Follower 則繼續採用異步同步。這保證了至少有兩個節點擁有最新數據,同時避免了被單一最慢節點拖垮的風險。

陷阱面試常考:異步複寫下的 Failover 數據丟失

在異步複寫下,Leader 宕機後,一個落後的 Follower 被提升為新 Leader。此時,舊 Leader 重新上線,它發現自己本地有一些新 Leader 沒有的寫入。如果直接同步,會導致資料衝突。常見的做法是強行捨棄(Discard)舊 Leader 上未同步的寫入,這在金融等對資料精確度要求極高的場景中是不可接受的。此時必須考慮使用雙階段提交(2PC)或 Raft/Paxos 等強一致性共識演算法。

一句話答法同步複寫保證不丟資料但犧牲寫入延遲與可用性;異步複寫追求極致效能但容忍 Replication Lag 與資料丟失風險;半同步則是只等一個副本確認的實務折衷。

04

複製延遲下的三大一致性異常

當物理世界的延遲發生時,應用層會看到哪些奇葩的 Bug?

在採用異步複寫的系統中,由於網路擁塞或節點負載,Follower 的同步進度可能落後 Leader 數秒甚至數分鐘。這種現象稱為 Replication Lag。
雖然系統最終會達到一致(Eventually Consistent),但在這段延遲窗口內,若用戶直接讀取落後的 Follower,就會觸發以下三種典型的一致性異常:

1. 讀寫一致性異常 (Read-After-Write Consistency Anomaly)

• 場景:用戶在社群媒體上發表了一篇貼文(寫入 Leader 成功),然後重新整理頁面。此時,讀取請求被路由到了一個尚未同步該貼文的 Follower 節點。
• 現象:用戶發現自己剛剛發表的貼文消失了!這會讓用戶產生極大的挫折感,誤以為系統出錯。
• 解決方案:
1. 特定路由:對於用戶自己修改的資料(如個人 Profile),強制路由到 Leader 讀取;非本人修改的資料則讀取 Follower。
2. 時間窗口守護:記錄用戶最後一次寫入的時間戳,在寫入後的特定窗口期內(如 5 秒內),該用戶的讀取請求強制路由至 Leader,或路由至同步進度已超越該時間戳的 Follower。

2. 單調讀取異常 (Monotonic Reads Anomaly)

• 場景:用戶 A 發表了兩條評論(1: "Hello", 2: "World")。用戶 B 重新整理頁面,第一次讀取路由到了進度較快的 Follower 1,看到了這兩條評論。接著用戶 B 再次整理頁面,讀取路由到了進度較慢的 Follower 2。
• 現象:用戶 B 第二次讀取時,發現評論 2 消失了。在用戶看來,系統出現了「時間倒流」的詭異現象。
• 解決方案:
1. 黏性路由 (Sticky Routing):確保同一個用戶的讀取請求總是路由到同一個副本(例如,對 User ID 進行 Hash 路由)。只有當該副本宕機時,才切換到其他副本。

3. 一致前綴讀取異常 (Consistent Prefix Reads Anomaly)

• 場景:在分片資料庫中,存在因果關係的兩筆寫入被分配到了不同的分片。例如,A 問:"你吃飽了嗎?"(分片 1),B 回答:"吃飽了。"(分片 2)。
• 現象:由於分片 1 與分片 2 的複寫延遲不同,第三方讀取者可能先看到 B 的回答 "吃飽了。",過了一陣子才看到 A 的提問。這違反了因果關係(Causality)。
• 解決方案:
1. 因果追蹤:確保任何具有因果關係的寫入都被分配到同一個分片中。
2. 邏輯時鐘/版本向量:在應用層利用邏輯時鐘重建事件的先後順序。

一句話答法Replication Lag 會引發讀寫一致性(看不到自己的更新)、單調讀(資料時間倒流)與一致前綴讀(因果順序錯亂)三大異常;可透過強制路由 Leader、黏性路由與因果追蹤來防禦。

05

互動演示:同一組寫入,半同步側與全異步側並排

拖動複寫延遲與讀取時刻,看三種異常是否真的發生,以及寫入延遲與遺失窗各付出多少。

前面兩節把取捨講完了,但「半同步到底買到了什麼」很容易被誤會。下面這個模擬器把同一組寫入同時餵給兩側:左邊是半同步(Leader 等最快的一個副本確認才回 Client),右邊是全異步(Leader 寫完本地立刻回)。兩側的副本複寫速度完全相同,唯一的差別是 Client 何時收到「成功」——而這個時間差會連帶推遲讀取的時刻,這正是半同步看起來「比較不會讀到舊值」的真正原因,而不是因為它把副本變快了。

三筆寫入依序是:貼文 #1「Hello」、貼文 #2「World」,以及一則回覆(因果上依賴貼文 #2)。打開「回覆寫到另一個分片」時,回覆會走第二條複寫串流,而該串流較快——這正是一致前綴讀取異常的溫床。

互動 · 半同步 vs 全異步 同一組輸入同時推到兩側
寫入間隔60 ms
Follower A 複寫延遲110 ms
Follower B 複寫延遲280 ms
寫完多久後重新整理40 ms
第二次讀的間隔60 ms
半同步
等最快的一個副本 ACK
全異步
Leader 寫完本地立刻回
Leader Follower A Follower B 讀取時刻 異常成立

階梯線的 Y 軸是該副本已看得見的「連續前綴」長度(0~3 筆)——第 2 筆沒到,就算第 3 筆先到了,前綴仍停在 1。虛線是兩次讀取的時刻,圓點落在被路由到的那條線上,兩點之間那條連線就是讀者感知:它往下掉一階,單調讀取異常就成立了。線下的小三角是「回覆」抵達各副本的時刻。

先看兩側的讀者感知連線:預設參數下,全異步側第一次就只讀到 1 筆(自己剛發的貼文不見了),半同步側第一次讀到 3 筆完整——但兩側的第二次讀都掉到 0 筆,單調讀取異常在半同步側照樣成立。半同步真正買到的是「遺失窗恆為 0」那一列,不是異常欄。路由選「輪詢」時第 1 次固定讀 A、第 2 次固定讀 B(不是隨機),這樣兩次讀的差異才歸因於副本進度而不是運氣。

一句話答法半同步用寫入延遲換掉的是資料遺失窗,不是 Stale Read——只要讀取還會落到落後的副本,單調讀與一致前綴異常照樣發生;真正修掉讀取異常的是路由策略(黏性路由、讀寫守護),兩者解決的是不同的問題。

06

面試考點與架構折衷

高頻面試題與系統架構師的思維框架。

在系統設計面試中,資料複寫幾乎是所有大型系統設計題(如「設計一個 Messenger」或「設計一個電商購物車」)的必考點。面試官不會只問你定義,而是會拋出具體的架構衝突,考驗你的折衷(Trade-offs)思維。

面試高頻問題與回答框架

考點 1如何處理 Replication Lag 帶來的 Stale Read 異常?

回答框架:
1. 分類討論:首先指出並非所有資料都需要強一致。對於非關鍵資料(如貼文按讚數),容忍最終一致性,不需要特別處理。
2. 用戶體驗防禦:對於關鍵資料(如剛發表的貼文、修改的密碼),採用讀寫一致性守護。最簡單的實作是:用戶讀取自己剛修改的資料時,強制路由到 Leader。
3. 時效性快取:在 Redis 中記錄一個臨時標記 user_just_updated:userId = true(過期時間設為預估的最大 Lag,如 5 秒)。只要標記存在,讀取請求就路由到 Leader,否則路由到 Follower。

考點 2單主複製中,Leader 宕機時的 Failover 流程與潛在風險是什麼?

回答框架:
1. 流程:心跳超時檢測 → 選舉新 Leader(通常選擇同步進度最接近、或 Raft Term 最高的節點) → 重新配置路由(通知 Client 與其他 Follower 指向新 Leader)。
2. 風險與折衷:
• 數據丟失:在異步複寫下,新 Leader 可能缺少舊 Leader 宕機前未同步的資料。
• 腦裂 (Split-brain):舊 Leader 重新上線,與新 Leader 衝突。必須使用 Quorum 租約或 Fencing Token 確保只有一個 Leader 具備寫入權限。
• 超時設定的兩難:心跳超時設太短會導致頻繁的誤判與不必要選舉(引發抖動);設太長則會增加系統的不可用時間。

架構師的折衷清單 (Trade-off Checklist)

設計決策 選擇 A (同步/強一致) 選擇 B (異步/最終一致) 折衷考量 (Trade-off)
複寫時序 同步複寫 (Synchronous) 異步複寫 (Asynchronous) Durability vs. Latency:金融交易選 A,社群媒體/日誌系統選 B。
讀取路由 全部路由至 Leader 讀寫分離 (Read from Followers) Consistency vs. Read Scalability:讀寫分離能大幅擴充讀取吞吐量,但必須承擔 Replication Lag 帶來的 Stale Read 異常。
寫入拓撲 單主複製 (Single-Leader) 無主複製 (Leaderless) Write Simplicity vs. Write Availability:單主複製無衝突但有寫入單點瓶頸;無主複製寫入極度可用但需要處理複雜的 Quorum 讀寫與版本衝突。
一句話答法沒有完美的複寫方案,只有最適合業務場景的折衷;面試中應從「資料關鍵度」出發,靈活運用特定路由、時效性快取與 Quorum 機制,在強一致與高可用之間取得平衡。