Database Scaling & High Availability
my-notes/system-design-hld/concepts · ZH
資料庫擴展與高可用性設計指南
💡 一、章節概要
當資料庫「太大、太慢、太忙」時, 我們要透過 資料設計(設計層)、效能優化(應用層)、與 系統擴展(架構層) 讓系統能穩定承載龐大流量,同時維持一致性與可用性。
核心目標:
- 提升資料存取效率(Scaling)
- 保證系統穩定不中斷(High Availability)
- 控制資料一致性、可用性、延遲之間的平衡(CAP / BASE)
🧱 二、資料設計層:正規化與反正規化
🔹 正規化(Normalization)
將資料結構「拆解到最乾淨的形態」, 以消除重複資料(Redundancy)並維持一致性(Consistency)。
| 階段 | 目標 | 說明 |
|---|---|---|
| 1NF(第一正規化) | 欄位不可再分割 | e.g. phone1, phone2 → 轉成另一張電話表 |
| 2NF(第二正規化) | 消除部分相依 | 讓非主鍵欄位完全依賴主鍵 |
| 3NF(第三正規化) | 消除傳遞相依 | e.g. 學生表不應包含系主任名字(屬於系表) |
| BCNF(Boyce-Codd NF) | 更嚴格的 3NF | 所有決定子都是候選鍵 |
📘 優點:
- 更新簡單、一致性高
- 節省儲存空間
⚠️ 缺點:
- JOIN 過多,查詢效率下降
- 分散式環境難以橫向擴展
🔹 反正規化(Denormalization)
為了效能而「違反部分正規化原則」, 將常用欄位或結果提前展開、預存、或重複。
📈 常見策略:
- 欄位冗餘:將 user_name, email 放入訂單表,減少 JOIN
- 預先彙總表(Aggregation Table):每日銷售總表
- Materialized View:快取查詢結果
- Flatten Document(NoSQL): 把嵌套文件攤平,加快讀取
⚠️ 缺點:
- 更新成本高(同一資料需多處同步)
- 儲存空間增加
- 不一致風險(Consistency trade-off)
🧩 常見應用:
- 讀多寫少(Read-heavy)系統,如報表、Feed 流、內容平台
- 寫入壓力低、但查詢複雜的場景
⚙️ 三、資料庫效能優化與擴展策略(SQL & NoSQL 通用)
1️⃣ Indexing — 提升查詢效率的第一步
索引是一種排序結構(通常為 B+ Tree), 讓查詢能快速定位資料列而非全表掃描。
📘 常見索引類型:
| 類型 | 特性 | 使用場景 |
|---|---|---|
| B+ Tree Index | 最常見的通用索引 | 篩選 / 排序欄位 |
| Hash Index | 快速等值查詢(=) | 高速 Key Lookup |
| Composite Index | 多欄位索引 | 頻繁條件組合查詢 |
| Covering Index | 查詢欄位全在索引中 | 無需回表 |
| Full-text Index | 文本搜尋 | 搜尋文章內容 |
| Spatial Index | 地理座標搜尋 | LBS, 地圖系統 |
🧠 實務建議:
-
80% 的效能問題來自索引設計不當
-
寫多讀少的系統:索引過多會拖慢寫入
-
查詢優化原則:
- EXPLAIN 分析執行計畫
- 避免 SELECT *
- 讓篩選條件(WHERE)與索引匹配順序一致
2️⃣ Replication(主從複製)— 讀寫分離
將主資料庫(Master)負責寫入, 將從資料庫(Replica)負責讀取, 分散負載並提升可用性。
[Write Requests] → Master DB
[Read Requests] → Replica DB(s)
✨ 優點:
- 可橫向擴展讀取效能
- 主掛掉時可快速切換(Failover)
- 支援跨地區容錯
⚠️ 缺點:
- 複製延遲(Replication Lag) → 可能讀到舊資料
- 主節點壓力集中於寫入
- 寫入一致性問題(需最終同步)
🧩 常見實作:
| 系統 | 機制 |
|---|---|
| MySQL | Binlog-based Replication, Group Replication |
| PostgreSQL | Streaming Replication, Logical Replication |
| MongoDB | Replica Set(自動選主) |
| Cassandra | Multi-master(Peer-to-peer replication) |
3️⃣ Failover(故障切換)與備援
當主資料庫掛掉時,自動讓副本升級為主節點,確保高可用(HA)。
🚀 流程:
- 監控主節點健康狀態(Health Check)
- 偵測故障 → 選出新的主節點
- 應用層(App)重新導向連線
🧰 工具實例:
- MySQL:MHA、Orchestrator、ProxySQL
- PostgreSQL:Patroni、Repmgr、PgBouncer
- MongoDB:Replica Set 自動切換
- Kubernetes:可用 StatefulSet + Operator 自動化切換
4️⃣ Sharding(資料分片)— 寫入與容量的最終解法
當資料量過大或寫入量太高, 需將資料拆分到多台節點(水平分片)。
💡 適用條件:
- 資料表達「數億級」
- 單 DB 延遲瓶頸
- QPS / IOPS 無法再提升
- 全球多地分佈部署
📘 常見分片策略:
| 類型 | 說明 | 優缺點 |
|---|---|---|
| Hash-based | 根據 user_id % N 分散到不同節點 | 分布均勻但難跨分片查詢 |
| Range-based | 根據時間或 ID 範圍劃分 | 易跨片查詢、但可能傾斜 |
| Directory-based | 外部服務紀錄分片位置(metadata) | 靈活但需額外維護 |
⚠️ 挑戰:
- 跨分片 JOIN、Transaction 不再簡單
- 資料重分佈(Resharding)成本高
- 需應用層協調一致性(Saga、Two-phase commit)
💬 實際應用:
| 系統 | 特性 |
|---|---|
| MongoDB Shard Cluster | Router + Config Server + Data Shards |
| MySQL Sharding | 應用層分片(分庫分表) |
| Cassandra | 自動一致性哈希分片 |
| Elasticsearch | Shard + Replica 架構自動管理 |
5️⃣ Vertical Partitioning(垂直分割)
將 表過寬(太多欄位)的問題拆分, 把常用欄位與不常用欄位分開,減少 I/O。
範例:
users_basic(id, username, email, last_login)
users_profile(id, avatar_url, bio, preferences)
📈 優點:查詢常用欄位更快 📉 缺點:仍需 JOIN(但成本降低)
💡 適合:用戶、商品、內容表(欄位眾多但常查部分)
6️⃣ Caching(快取)— 應用層的「第一道防線」
在應用層或資料庫層加入快取系統(如 Redis / Memcached), 減少重複查詢、降低延遲。
App → Cache → DB
📘 策略:
| 機制 | 說明 | 解法 |
|---|---|---|
| Cache TTL(過期時間) | 控制資料新鮮度 | 熱點資料可延長 TTL |
| Cache Stampede(擊穿) | 熱點快取同時失效導致大量 DB 請求 | 使用分佈式鎖 / 預熱機制 |
| Cache Snowball(雪崩) | 多筆快取同時失效 | 分散 TTL 時間、批次重建 |
| Cache Penetration(穿透) | 查詢不存在資料 | 加空值快取或布隆過濾器(Bloom Filter) |
📦 工具:
- Redis(In-memory key-value store)
- Memcached(純快取)
- CDN Cache(靜態內容)
🧭 四、NoSQL 系統的可擴展性與高可用設計
| 系統 | 擴展機制 | 一致性模型 | 高可用設計 |
|---|---|---|---|
| MongoDB | Sharding + Replica Set | Eventual Consistency | 自動選主與複製 |
| Cassandra | Peer-to-peer + Gossip | Tunable Consistency | 多節點寫入容錯 |
| Redis Cluster | Slot-based Partitioning | 最終一致 | 主從 + Sentinel Failover |
| Elasticsearch | Shard + Replica | Eventual | 自動重建節點、容錯 |
💡 特點:
- NoSQL 多採 BASE 模型(最終一致)
- 天生支援 水平擴展(Scale-out)
- 寫入與查詢可分散至多節點
⚖️ 五、SQL vs NoSQL 在擴展與可用性上的取捨
| 面向 | SQL | NoSQL |
|---|---|---|
| 一致性 | 強一致(ACID) | 弱一致(BASE) |
| 可用性 | 主從 + 備援 | 多主、多節點容錯 |
| 擴展方式 | 垂直為主(Scale-up) | 水平為主(Scale-out) |
| 快取依賴度 | 高(Cache as Layer) | 中(部分自帶 cache) |
| 交易支援 | 完整 Transaction | 限定或簡化 |
| 最佳場景 | 金融、交易、報表 | 高併發、社群、分散式系統 |
🧩 六、綜合架構示意
┌───────────────┐
│ Application │
├───────────────┤
│ Cache Layer │ ← Redis / CDN
├───────────────┤
│ Read Replica 1 │ ← 讀取
│ Read Replica 2 │
├───────────────┤
│ Master DB │ ← 寫入
├───────────────┤
│ Shard A │
│ Shard B │ ← 水平分片
├───────────────┤
│ Backup / HA │ ← 備援、快照
└───────────────┘
🧠 七、設計原則總結
| 問題 | 解法優先順序 |
|---|---|
| 查詢慢 | Index → Denormalization → Cache |
| 讀取壓力大 | Replication + Read/Write Split |
| 單機瓶頸 | Sharding(最後手段) |
| 表太寬 | Vertical Partitioning |
| 高可用 | Replica + Failover |
| 高延遲 | Cache + CDN |
| 一致性要求高 | SQL / CP 模型 |
| 高併發、低延遲 | NoSQL / AP 模型 |
✅ 八、總結一句話
Database Scaling & High Availability = 效能 + 穩定 + 一致的取捨藝術。
-
設計層:正規化 → 資料乾淨
-
效能層:索引 + 快取 → 加速查詢
-
架構層:讀寫分離 + 備援 + Sharding → 撐起高流量
📖 在系統設計中, Sharding 永遠是最後的手段,Cache 永遠是第一道防線。
Denormalization 反正規化 範例
**🔹 原始(正規化)設計 **
訂單系統
| 表格 | 欄位 |
|---|---|
| Users | user_id, name, email |
| Orders | order_id, user_id, total_price |
| OrderItems | order_id, product_id, qty |
| Products | product_id, name, price |
要查詢「訂單 + 使用者名稱 + 商品名稱」, 需要至少 JOIN 三張表:
SELECT o.order_id, u.name, p.name, p.price
FROM orders o
JOIN users u ON o.user_id = u.user_id
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id;
📉 問題:
- 需要多重 JOIN
- 高併發下 CPU / I/O 成本大
🔹 反正規化後的版本
我們可以把常用欄位「展開」存入訂單表中:
| order_id | user_id | user_name | product_name | total_price |
|---|---|---|---|---|
| 1001 | 1 | Alice | Keyboard | 50 |
| 1002 | 1 | Alice | Mouse | 20 |
| 1003 | 2 | Bob | Laptop | 900 |
| 📈 查詢變快: |
SELECT order_id, user_name, product_name, total_price
FROM denormalized_orders
WHERE user_id = 1;
📉 代價:
- 若 user_name 改變,所有訂單記錄都要更新。
- 資料不一致風險上升。
🧠 四、常見的反正規化策略
| 類型 | 說明 | 範例 |
|---|---|---|
| 欄位冗餘 | 將常查欄位複製到主表 | 在 Orders 表中存 user_name |
| 彙總表(Summary Table) | 預先計算結果 | 每日銷售額、用戶統計表 |
| Materialized View | 將查詢結果快取成實體表 | PostgreSQL、Oracle 支援 |
| 扁平化結構(Flattening) | JSON 結構展開 | NoSQL 中將巢狀物件攤平 |
| Precomputed Columns | 儲存計算結果 | 儲存 total_price 而非每次 SUM() |
| 嵌入子文件(Embedding) | 文件導向資料庫(MongoDB)中把關聯資料直接存進同一文件 | User 文件內嵌 Address 子文件 |
📘 五、實際案例:反正規化查詢速度提升
假設要查詢「每位用戶的總消費金額」
(1) 正規化設計:
SELECT u.name, SUM(o.total_price)
FROM users u
JOIN orders o ON u.id = o.user_id
GROUP BY u.name;
📉 每次都要掃描兩張表並 GROUP。
(2) 反正規化設計(預先維護彙總表):
| user_id | total_spent |
|---|---|
| 1 | 70 |
| 2 | 900 |
📈 查詢速度可提升 數十倍:
SELECT total_spent FROM user_stats WHERE user_id = 1;
更新策略:
- 每次訂單提交時同步更新該用戶的 total_spent
- 或每日批次重算
🗃️ 六、反正規化在 NoSQL 的應用
在 NoSQL(特別是 Document DB)中, 反正規化幾乎是預設設計哲學。
📘 MongoDB 範例
(正規化方式)
User = { _id: 1, name: "Alice" }
Post = { _id: 10, user_id: 1, title: "Hello" }
查詢貼文 + 使用者名稱 → 需要 $lookup(JOIN 等效操作)
(反正規化方式)
Post = {
_id: 10,
title: "Hello",
user: { id: 1, name: "Alice" }
}
📈 查詢只需一次讀取,效能極佳。 📉 缺點:若使用者名稱改變,所有貼文都需更新
💡 MongoDB 設計原則:
「Data that is accessed together should be stored together.」 (會一起被查詢的資料,應該一起儲存)
⚖️ 七、反正規化的優缺點
| 面向 | 優點 | 缺點 |
|---|---|---|
| 效能 | 大幅降低 JOIN 開銷、查詢速度快 | 更新成本高 |
| 一致性 | 弱一致(需自行同步) | 資料重複 |
| 維護性 | 查詢簡單 | Schema 複雜、難追蹤更新 |
| 儲存空間 | 更多資料冗餘 | 成本增加 |
🧩 八、反正規化 + Cache = 實務強化組合
在大型系統中,反正規化往往搭配快取層(Redis、Memcached):
Request → Cache → (Denormalized Table) → Database
- Cache 作為熱資料加速層
- 反正規化作為「冷資料快取」或中間層
- 結合兩者可大幅提升 QPS 與響應時間
📘 範例:
電商後台查詢「使用者最新訂單」 → 先查 Redis(快取) → 若無快取則查 Denormalized Table(讀快) → 再同步主表(寫慢)
🧮 九、反正規化的維護策略
| 類型 | 方法 | 工具 |
|---|---|---|
| 同步更新 | 應用層同時更新多表 | 交易控制(ACID) |
| 非同步更新 | 使用 Queue(Kafka, RabbitMQ)或定期 Job | 延遲一致性 |
| 批次重建 | 每日重算彙總表 | ETL / Airflow / Spark |
| 版本欄位 | 用 updated_at 追蹤同步狀態 | ETL 驗證一致性 |
🧠 十、在系統設計中的定位
| 系統規模 | 設計策略 |
|---|---|
| 小規模系統 | 優先正規化(維持簡潔) |
| 中規模(10萬級用戶) | 適度反正規化(減少 JOIN) |
| 大規模(億級資料) | 全面反正規化 + Cache + Sharding |
| 分散式應用 | 採 BASE 模型(最終一致) |
✅ 十一、一句話總結
正規化 解決「資料一致性」問題。 反正規化 解決「查詢效能」問題。
⚙️ 沒有絕對好壞,只有依系統特性選擇取捨。
📊 寫多讀少 → 正規化 ⚡ 讀多寫少 → 反正規化 🌐 分散式系統 → 結合快取與事件同步機制
⚙️ Sharding 通常用在哪種資料庫?
✅ Sharding 是為了「寫入與容量的擴展」而設計的。 也就是說,它主要針對「寫的資料庫」。
1️⃣ 「讀」的瓶頸可以靠 Replication(主從複製)解決
- 讀取操作天生可平行化
- 你可以有多個 replica 同時服務查詢
- 這時只要做 讀寫分離(Read/Write Split) 就能橫向擴展
- 所以 讀的壓力通常不需要 sharding
📘 例如:
App
├──> Master DB (寫)
├──> Read Replica A (讀)
└──> Read Replica B (讀)
👉 當「讀取」變多時,只要增加 replica 就好,不需要動資料結構。
2️⃣ 「寫」的瓶頸無法靠複製解決
- 所有寫入仍集中在主資料庫(Single Write Master)
- 單台機器的 IOPS、CPU、磁碟空間都有上限
- Replication 只能複製結果,不能分攤寫入壓力
📉 這時候就必須:
➤ 將資料「分片(Shard)」到多個節點, 讓不同的使用者或業務區塊能同時寫入不同資料庫。
📊 比較:Replication vs Sharding
| 項目 | Replication(主從複製) | Sharding(資料分片) |
|---|---|---|
| 主要目標 | 增加讀取吞吐量 | 增加寫入與容量吞吐量 |
| 資料內容 | 每個節點都存同一份資料 | 每個節點只存部分資料 |
| 一致性 | 主寫、從讀(延遲同步) | 每片獨立維護自己的資料 |
| 擴展方式 | 水平擴展「讀」 | 水平擴展「寫」與「容量」 |
| 複雜度 | 相對簡單 | 架構與查詢邏輯更複雜 |
| 適用時機 | 讀多寫少(read-heavy) | 寫多或資料量極大(write-heavy / data-heavy) |
🧩 真實案例
| 系統 | 方案 | 原因 |
|---|---|---|
| MySQL 電商系統 | Sharding by user_id | 不同使用者訂單同時寫入不同分片 |
| 社群平台(Twitter) | Shard by tweet_id(時間序號) | 高速寫入 tweet log,橫向擴展 |
| 遊戲後台 | Shard by region | 每伺服器區玩家資料獨立儲存 |
| 金融系統 | Shard by account_id | 避免單主庫寫入瓶頸 |
| MongoDB | 自動分片(Shard Key) | 同時提升容量與寫入效能 |
💡 延伸設計:讀寫都很多時怎麼辦?
若系統既「讀多」又「寫多」,可同時結合兩種策略:
App
├──> Shard A (Master + Replicas)
├──> Shard B (Master + Replicas)
├──> Shard C (Master + Replicas)
這叫做:
Sharded Cluster with Replication 每個 shard 各自有 replication group, 既能水平分寫,也能水平分讀。
📘 MongoDB、Cassandra、Elasticsearch 都是這種架構。
🧭 五、一句話總結
-
Replication → 解決讀的問題(Read Scaling)
-
Sharding → 解決寫的問題(Write Scaling)
📖 Sharding 幫你分散「資料與寫入壓力」, 不是用來分散查詢的。