【系統設計白話文】#05 資料庫設計與擴展策略 — 分片、複製與 ACID

測驗:資料庫設計與擴展策略 — 分片、複製與 ACID

共 5 題,點選答案後會立即顯示結果

1. ACID 中的「原子性(Atomicity)」是什麼意思?

  • A. 資料庫的操作速度要像原子一樣快
  • B. 一個交易裡的所有操作,要嘛全部完成,要嘛全部取消
  • C. 每次交易只能操作一筆資料
  • D. 資料完成寫入後就永久保存,不會遺失

2. 垂直擴展(Scale Up)和水平擴展(Scale Out)的主要區別是什麼?

  • A. 垂直擴展加更多機器,水平擴展升級單台機器
  • B. 垂直擴展只能用在資料庫,水平擴展適用於所有系統
  • C. 垂直擴展是升級單台機器的硬體,水平擴展是增加更多機器
  • D. 垂直擴展沒有上限,水平擴展有硬體瓶頸

3. 在資料庫複製的主從架構中,讀寫分離為什麼能提升效能?

  • A. 因為從庫的硬體比主庫更好
  • B. 因為寫入操作比讀取操作消耗更多資源
  • C. 因為主庫和從庫使用不同的資料庫軟體
  • D. 因為大部分應用讀多寫少,讀取壓力可以分散到多台從庫

4. Hash-based 分片策略的主要優點和缺點分別是什麼?

  • A. 優點是範圍查詢效率高,缺點是資料分佈不均
  • B. 優點是資料分佈均勻,缺點是範圍查詢困難且增減分片時需大量資料搬遷
  • C. 優點是最靈活可隨意調整,缺點是對照表成為單點故障
  • D. 優點是實作簡單,缺點是只適用於數字型態的資料

5. 某電商平台的訂單系統需要選擇複製策略。訂單金額必須精確、不能有延遲誤差。應該選擇哪種複製方式?

  • A. 同步複製,因為主庫會等所有從庫確認後才回覆成功,確保資料強一致
  • B. 非同步複製,因為速度更快,使用者體驗更好
  • C. 非同步複製,因為最終一致性對訂單系統已經足夠
  • D. 兩種都可以,複製方式不影響資料正確性

**系列**:系統設計白話文(第 5 篇,共 5 篇 — 系列最終篇)
**難度**:L2-進階
**前置知識**:【系統設計白話文】#04 快取、CDN 與流量管理
**影片來源**:[freeCodeCamp — System Design Concepts Course and Interview Prep(Hayk Simonyan)](https://www.youtube.com/watch?v=F2FmTdLtb_4)


一句話說明

資料庫是系統的「記憶」,ACID 確保資料正確,分片和複製讓資料庫撐住大流量。


這篇在講什麼?

前四篇我們從硬體、網路、協定、快取一路走來。但不管你怎麼快取、怎麼做 CDN,最終資料都要落地到資料庫。當你的使用者從 100 人成長到 1000 萬人,一台資料庫伺服器遲早會扛不住。

這篇要回答三個問題:

  1. 怎麼確保資料不會寫壞? → ACID
  2. 一台不夠怎麼辦? → 擴展策略(垂直 vs 水平)
  3. 資料太多怎麼切?讀取太慢怎麼分? → 分片(Sharding)與複製(Replication)

ACID:資料庫的四大保證

白話版:銀行轉帳的故事

想像你要從帳戶 A 轉 1000 元到帳戶 B。這個操作涉及兩步:

步驟 1:帳戶 A 扣 1000 元
步驟 2:帳戶 B 加 1000 元

如果步驟 1 做完、步驟 2 還沒做,系統就當機了呢?1000 元就「憑空消失」了。ACID 就是為了防止這種事發生。

四大特性一次看懂

特性 英文 白話翻譯 銀行轉帳的例子
原子性 Atomicity 全做或全不做 扣款和入帳要嘛都成功,要嘛都不做
一致性 Consistency 交易前後資料都合理 轉帳前後,A + B 的總額不變
隔離性 Isolation 同時交易互不干擾 你轉帳的同時,別人也在轉,彼此不會搞混
持久性 Durability 完成就永久保存 轉帳成功後就算斷電,資料還是在

逐個拆解

Atomicity(原子性)—— 全做或全不做

交易開始
  帳戶 A: 5000 → 4000  (扣 1000)
  帳戶 B: 3000 → 4000  (加 1000)
交易結束 ✅

如果中途失敗:
  帳戶 A: 5000 → 5000  (回滾,當作什麼都沒發生)
  帳戶 B: 3000 → 3000  (回滾)

「原子」就是「不可分割」。一個交易裡的所有操作,要嘛全部完成,要嘛全部取消。不會有「做一半」的情況。

Consistency(一致性)—— 資料始終合理

轉帳前:A(5000) + B(3000) = 8000
轉帳後:A(4000) + B(4000) = 8000  ✅ 總額沒變

不合理的情況:
轉帳後:A(4000) + B(3000) = 7000  ❌ 少了 1000 塊!

一致性確保每次交易都讓資料從「一個合理的狀態」轉移到「另一個合理的狀態」。違反業務規則的操作會被拒絕。

Isolation(隔離性)—— 交易之間互不打擾

時間線:
User X: [開始轉帳 A→B] -------- [完成]
User Y:     [開始轉帳 A→C] -------- [完成]

隔離性確保:
即使 XY 同時操作帳戶 A,
兩筆交易看到的帳戶餘額不會互相干擾。
Code language: CSS (css)

沒有隔離性的話,兩個人同時讀到 A 有 5000 元,各轉 3000 元出去,帳戶 A 就會變成 -1000 元。

Durability(持久性)—— 寫了就不會丟

交易完成 → 寫入磁碟 → 回覆「成功」

即使下一秒:
- 斷電 ⚡ → 重啟後資料還在
- 伺服器當機 💥 → 恢復後資料還在

資料庫不是只把資料放在記憶體裡。一旦交易完成,資料就已經安全地寫入了永久儲存。

必看懂 vs 知道就好

✅ 必看懂(面試、設計都會用到):
- 原子性:全做或全不做
- 一致性:資料前後都合理
- 隔離性:同時操作不互相干擾
- 持久性:完成就永久保存

📌 知道就好(遇到再查):
- 隔離級別(Read Uncommitted, Read Committed, Repeatable Read, Serializable)
- WAL(Write-Ahead Logging)的實作細節
- MVCC(多版本並發控制)

擴展策略:一台不夠,怎麼加?

當資料庫撐不住了,有兩種方向可以走。

垂直擴展(Scale Up):讓一台機器更強

升級前:                    升級後:
┌─────────────────┐       ┌─────────────────┐
│  DB Server      │       │  DB Server      │
│  4 CPU          │  →    │  32 CPU         │
│  16 GB RAM      │       │  256 GB RAM     │
│  500 GB SSD     │       │  4 TB NVMe SSD  │
└─────────────────┘       └─────────────────┘

白話翻譯:原本的電腦不夠快?換一台更好的。

優點 缺點
簡單,不用改程式 有硬體上限(再貴也買不到無限 RAM)
資料不用搬家 單點故障(這台掛了全掛)
不會有資料一致性問題 越高階越貴,價格非線性成長

水平擴展(Scale Out):加更多機器

擴展前:                    擴展後:
┌──────────┐              ┌──────────┐
│ DB Server│              │ DB Server 1 │
└──────────┘              ├──────────┤
                          │ DB Server 2 │
     1 台                 ├──────────┤
                          │ DB Server 3 │
                          ├──────────┤
                          │ DB Server 4 │
                          └──────────┘
                               4 台

白話翻譯:一個人搬不動?找更多人一起搬。

優點 缺點
理論上無上限 架構複雜度大增
沒有單點故障 跨機器的資料一致性難處理
可以用便宜的機器 需要決定「資料放哪台」

怎麼選?

使用者 < 10 萬:垂直擴展通常就夠了
使用者 > 100 萬:該考慮水平擴展了
實際上:大部分系統兩者都會用,先垂直再水平
Code language: HTML, XML (xml)

分片(Sharding):把資料切開放到多台機器

分片是水平擴展的核心手段。把一張大表的資料,按照某個規則,分散到多台機器上。

一看就懂的比喻

沒有分片:
┌─────────────────────────────┐
│ 全校 3000 個學生的資料      │
│ 全部放在一個檔案櫃裡        │  ← 找資料很慢
└─────────────────────────────┘

有分片:
┌───────────┐ ┌───────────┐ ┌───────────┐
│ A ~ H 姓氏│ │ I ~ P 姓氏│ │ Q ~ Z 姓氏│
│ 1000 人   │ │ 1000 人   │ │ 1000 人   │
└───────────┘ └───────────┘ └───────────┘
  Shard 1       Shard 2       Shard 3
                                ← 每個櫃子只需要翻 1/3 的資料

三種分片策略

1. Range-based(範圍分片)

按 user_id 的範圍切:

Shard 1: user_id 1 ~ 1,000,000
Shard 2: user_id 1,000,001 ~ 2,000,000
Shard 3: user_id 2,000,001 ~ 3,000,000
  • 優點:直覺,範圍查詢效率高(例如「找 id 100~200 的使用者」)
  • 缺點:容易不均勻。如果新註冊的使用者比較活躍,Shard 3 的流量會遠大於 Shard 1

2. Hash-based(雜湊分片)

hash(user_id) % 分片數 = 要放到哪個分片

hash(12345) % 3 = 0  → Shard 1
hash(67890) % 3 = 1  → Shard 2
hash(11111) % 3 = 2  → Shard 3
  • 優點:資料分佈均勻
  • 缺點:範圍查詢變困難(id 100~200 的使用者可能散落在不同分片),增減分片時需要大量資料搬遷

3. Directory-based(目錄分片)

維護一個對照表:

┌──────────┬─────────┐
│ user_id  │  shard  │
├──────────┼─────────┤
│ 12345    │ Shard 2 │
│ 67890    │ Shard 1 │
│ 11111    │ Shard 3 │
└──────────┴─────────┘

查詢時先查對照表,再去對應的分片找資料
  • 優點:最靈活,可以隨意調整
  • 缺點:對照表本身成為瓶頸和單點故障

三種策略比一比

策略 分佈均勻度 範圍查詢 靈活度 複雜度
Range-based 可能不均 很好
Hash-based 均勻 不好
Directory-based 可控 依設計

分片的挑戰

分片不是免費的午餐,它帶來這些問題:

1. 跨分片查詢(Cross-shard Query)

「找出所有超過 30 歲的使用者」

沒分片:一條 SQL 搞定
有分片:要對每個分片都查一次,再把結果合併

Shard 1 → 查詢 → 結果 A
Shard 2 → 查詢 → 結果 B    → 合併 → 最終結果
Shard 3 → 查詢 → 結果 C

2. 資料重新平衡(Rebalancing)

原本 3 個分片,現在要擴展到 5 個:

舊:[Shard 1] [Shard 2] [Shard 3]
新:[Shard 1] [Shard 2] [Shard 3] [Shard 4] [Shard 5]

需要把部分資料從舊分片搬到新分片,
搬遷期間系統還要繼續運作 → 非常複雜
Code language: CSS (css)

3. 跨分片的 JOIN 操作

「找出使用者和他的訂單」

如果 User 在 Shard 1,Order 在 Shard 3:
→ 無法用一般的 SQL JOIN
→ 需要在應用層自己合併

複製(Replication):同樣的資料,多存幾份

分片是把資料「切開」,複製是把資料「複製」。兩者解決不同問題。

分片(Sharding):資料 A 在機器 1,資料 B 在機器 2  → 解決「資料量太大」
複製(Replication):資料 A 在機器 1,資料 A 也在機器 2  → 解決「讀取太慢」和「可靠性」

主從架構(Primary-Replica)

                    寫入
使用者 ───────────→ ┌──────────┐
                    │  Primary │ ──── 同步 ────→ ┌──────────┐
                    │ (主庫)  │                  │ Replica 1│
                    └──────────┘ ──── 同步 ────→ │ (從庫)  │
                                                  ├──────────┤
使用者 ←── 讀取 ─── Replica 1                    │ Replica 2│
使用者 ←── 讀取 ─── Replica 2                    │ (從庫)  │
                                                  └──────────┘

核心原則

  • 寫入:只寫到主庫(Primary)
  • 讀取:從任何一個從庫(Replica)讀
  • 同步:主庫的變更會同步到所有從庫

讀寫分離:為什麼有用?

大部分應用的讀寫比例大約是 80% 讀、20% 寫

沒有讀寫分離:
所有讀 + 所有寫 → 1 台 DB → 扛不住

有讀寫分離:
所有寫 → Primary(1 台)
所有讀 → Replica(3 台分攤)  → 讀取能力提升 3 倍

這就是為什麼複製可以大幅提升效能 — 因為「讀」的壓力分散到了多台機器。

同步 vs 非同步複製

同步複製 非同步複製
做法 主庫等所有從庫確認才回覆「成功」 主庫寫完立刻回覆「成功」,背景同步
速度 較慢(要等) 較快(不等)
一致性 強一致(從庫永遠最新) 最終一致(從庫可能短暫落後)
適合 金融、訂單等不能出錯的場景 社群動態、文章瀏覽數等允許短暫延遲的場景
同步複製的時間線:
Client → Primary(寫入) → Replica 1(確認) → Replica 2(確認) → 回覆 Client ✅
                                                                      ↑ 全部確認才回覆

非同步複製的時間線:
Client → Primary(寫入) → 回覆 Client ✅
                          ↓(背景慢慢同步)
                     Replica 1... Replica 2...
                                  ↑ 可能有幾毫秒到幾秒的延遲

必看懂 vs 知道就好

✅ 必看懂:
- 主從架構:一台寫,多台讀
- 讀寫分離:讀多寫少時效果最好
- 同步 vs 非同步:速度和一致性的取捨

📌 知道就好:
- Multi-Primary(多主架構):多台都能寫,衝突處理更複雜
- 半同步複製(Semi-synchronous):只等一台從庫確認
- 故障轉移(Failover):主庫掛了,自動讓從庫升級為主庫

分片 + 複製:實戰中的組合技

真實世界不會只用一種策略。分片和複製通常一起使用:

┌──────────────────────────────────────────┐
│                  應用程式                  │
│                    │                      │
│         ┌─────────┴──────────┐           │
│         ▼                    ▼            │
│   ┌──────────┐        ┌──────────┐       │
│   │ Shard 1  │        │ Shard 2  │       │
│   │ (A ~ M)  │        │ (N ~ Z)  │       │
│   └────┬─────┘        └────┬─────┘       │
│   ┌────┴─────┐        ┌────┴─────┐       │
│   │ Primary  │        │ Primary  │       │
│   │ Replica1 │        │ Replica1 │       │
│   │ Replica2 │        │ Replica2 │       │
│   └──────────┘        └──────────┘       │
└──────────────────────────────────────────┘

每個分片都有自己的主從架構
→ 同時解決「資料量大」和「讀取多」的問題

Vibe Coder 檢查點

當你在看系統架構圖或讀程式碼時,看到資料庫相關的設計,確認以下幾點:

  • [ ] 看到「Transaction」或「交易」→ 想到 ACID,這段操作是否需要全做或全不做?
  • [ ] 看到資料庫連線設定有多個 host → 可能是讀寫分離,主庫寫、從庫讀
  • [ ] 看到 shard_keypartition → 正在做分片,注意跨分片查詢的限制
  • [ ] 看到 read_replicareadonly 的連線 → 讀寫分離,這些連線只能讀
  • [ ] 看到「eventually consistent」→ 用的是非同步複製,資料可能有短暫延遲

系列總結:五篇串成一張知識地圖

恭喜你走到了系列最終篇。讓我們把五篇文章串起來,看看整個系統設計的全貌:

使用者發出請求
     │
     ▼
 ┌──────────────────────────────────────┐
 │  #01 硬體與作業系統基礎               │
 │  CPU、記憶體、硬碟、程序與執行緒       │
 │  → 理解「電腦怎麼跑程式」              │
 └───────────────┬──────────────────────┘
                 │ 請求透過網路傳送
                 ▼
 ┌──────────────────────────────────────┐
 │  #02 網路基礎與 DNS                   │
 │  IP、TCP/UDP、DNS 解析                │
 │  → 理解「請求怎麼到達伺服器」          │
 └───────────────┬──────────────────────┘
                 │ 伺服器收到請求
                 ▼
 ┌──────────────────────────────────────┐
 │  #03 API 與通訊協定                   │
 │  HTTP、REST、GraphQL、WebSocket       │
 │  → 理解「前後端怎麼溝通」              │
 └───────────────┬──────────────────────┘
                 │ 要處理大量請求
                 ▼
 ┌──────────────────────────────────────┐
 │  #04 快取、CDN 與流量管理              │
 │  快取策略、CDN、負載均衡               │
 │  → 理解「怎麼讓系統更快更穩」          │
 └───────────────┬──────────────────────┘
                 │ 資料要存起來
                 ▼
 ┌──────────────────────────────────────┐
 │  #05 資料庫設計與擴展策略(本篇)      │
 │  ACID、分片、複製、讀寫分離            │
 │  → 理解「怎麼安全又快速地存取資料」    │
 └──────────────────────────────────────┘
Code language: PHP (php)

關鍵概念一覽表

篇章 核心問題 關鍵概念
#01 硬體基礎 電腦怎麼跑程式? CPU、RAM、程序、執行緒
#02 網路基礎 請求怎麼到達伺服器? IP、TCP、DNS
#03 API 協定 前後端怎麼溝通? REST、GraphQL、WebSocket
#04 效能優化 怎麼讓系統更快更穩? 快取、CDN、負載均衡
#05 資料層 怎麼安全快速存取資料? ACID、分片、複製

下一步怎麼走?

這五篇涵蓋了系統設計的基礎知識。如果你想繼續深入,可以往這些方向探索:

  • 訊息佇列(Message Queue):系統之間的非同步溝通
  • 微服務架構(Microservices):把一個大系統拆成多個小服務
  • 分散式系統的取捨(CAP 定理):一致性、可用性、分區容錯不可兼得
  • 監控與可觀測性(Observability):怎麼知道系統是否健康

本篇重點回顧

概念 一句話總結
ACID 資料庫的四大安全保證:全做或全不做、資料合理、交易不干擾、寫了不丟
垂直擴展 換更好的機器,簡單但有上限
水平擴展 加更多機器,無上限但複雜
分片 把資料切到多台機器,解決資料量太大的問題
複製 同樣的資料多存幾份,解決讀取太慢和可靠性的問題
讀寫分離 主庫負責寫、從庫負責讀,適合讀多寫少的場景

系統設計不是背誦答案,而是理解每個選擇背後的取捨。沒有完美的架構,只有適合當前需求的架構。

進階測驗:資料庫設計與擴展策略 — 分片、複製與 ACID

測驗目標:驗證你是否能在實際情境中應用所學。
共 5 題,包含情境題與錯誤診斷題。

1. 你正在設計一個社群媒體平台的資料庫架構。目前使用者有 500 萬人,預計一年內成長到 5000 萬人。讀取流量遠大於寫入(約 90% 讀、10% 寫),但單台資料庫已經撐不住了。你的第一步擴展策略應該是什麼? 情境題

  • A. 直接對資料做分片(Sharding),將使用者資料切分到多台機器
  • B. 先做資料庫複製(Replication)加讀寫分離,用從庫分攤讀取壓力
  • C. 垂直擴展,直接把伺服器升級到最高規格
  • D. 同時做分片和複製,一次到位避免二次改造

2. 你負責一個電商平台的訂單資料庫,目前有 3 億筆訂單資料。團隊決定採用分片策略。訂單資料的主要查詢模式是:(1) 按使用者 ID 查詢該使用者的所有訂單 (2) 偶爾需要按日期範圍查詢所有訂單。你會選擇哪種分片策略? 情境題

  • A. Range-based,按訂單建立日期範圍分片
  • B. Directory-based,維護一張對照表記錄每筆訂單在哪個分片
  • C. Hash-based,按使用者 ID 做雜湊分片,讓同一使用者的訂單落在同一分片
  • D. Hash-based,按訂單 ID 做雜湊分片,確保資料均勻分佈

3. 你的團隊正在為一個線上銀行系統選擇資料庫複製策略。系統需要處理轉帳交易,帳戶餘額必須即時準確。同時,系統也有一個報表功能,用來生成每日交易統計。你會如何設計複製架構? 情境題

  • A. 全部使用同步複製,確保所有資料都強一致
  • B. 全部使用非同步複製,提升系統回應速度
  • C. 不做複製,只用一台資料庫確保資料一致性
  • D. 交易相關用同步複製確保餘額精確,報表查詢導向非同步複製的從庫以分散壓力

4. 一個電商網站使用 Hash-based 分片,原本有 3 個分片。因為業務成長,團隊新增了第 4 個分片,但上線後發現大量使用者查不到自己的歷史訂單。最可能的原因是什麼? 錯誤診斷

原本:hash(user_id) % 3 = 分片編號 新增後:hash(user_id) % 4 = 分片編號 例如 user_id = 12345: 原本:hash(12345) % 3 = 1 → 資料在 Shard 2 新增後:hash(12345) % 4 = 3 → 去 Shard 4 找 → 找不到!
  • A. 新的分片伺服器硬體規格不同,導致雜湊計算結果不一致
  • B. 分片數從 3 變成 4,雜湊取餘的結果改變,導致查詢被導向錯誤的分片,但資料仍在舊分片上
  • C. 新分片的資料庫索引尚未建立完成
  • D. 使用者的連線被負載均衡器導向了錯誤的伺服器

5. 一個社群平台使用主從複製加讀寫分離。使用者回報:「我剛發了一則貼文,重新整理頁面卻看不到,過幾秒再刷新才出現。」系統架構如下所示。最可能的原因是什麼? 錯誤診斷

寫入路徑:使用者發文 → Primary(主庫) 讀取路徑:使用者刷新 → Replica(從庫) 複製模式:非同步複製
  • A. Primary 主庫的寫入操作失敗了,資料沒有成功寫入
  • B. 使用者的瀏覽器快取了舊頁面,沒有發出新的請求
  • C. 非同步複製有延遲,使用者寫入主庫後立即從從庫讀取,但從庫尚未同步到最新資料
  • D. ACID 的隔離性導致其他使用者的交易阻擋了這筆寫入

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *