資料庫擴展(Database Scaling)
是什麼?
當單一資料庫撐不住流量或資料量時,需要透過 Replication 和 Sharding 來擴展。這是 System Design 中最關鍵也最複雜的環節。
ℹ️兩種擴展方向
Replication = 同一份資料複製多份,提高讀取能力和可用性。 Sharding = 把資料切分到多台機器,提高寫入能力和儲存容量。
核心觀念
Replication(複製)
- Master-Slave(主從複製):寫入只走 Master,讀取可以走多個 Slave。讀寫分離的基礎。適用判斷:讀寫比超過 5:1 且資料庫 CPU 使用率持續超過 60%
- Master-Master:多台都能寫入,但衝突解決複雜(兩台同時修改同一筆資料怎麼辦?)。適用場景:跨地域部署(美國和亞洲各一個 Master,就近寫入降低延遲),但必須有明確的衝突解決策略(如 Last Write Wins 或業務層合併)
- 同步 vs 非同步複製:同步複製 — Master 等所有 Slave 確認才回應客戶端,保證一致性但寫入延遲增加 2-5ms。非同步複製 — Master 寫完立刻回應,Slave 背景追趕,快但 Slave 可能延遲數毫秒到數秒(高負載時更久)。選擇標準:金融交易用同步,社群內容用非同步
- Failover:Master 掛掉時自動將 Slave 提升為新 Master。具體操作:MySQL 用 MHA 或 Orchestrator 自動切換,PostgreSQL 用 Patroni + etcd。手動 Failover 的 RTO(恢復時間)通常 10-30 分鐘,自動 Failover 可縮短到 10-30 秒
- 何時該做 Replication:單台資料庫的讀取 QPS 超過 5,000、需要高可用(單台掛掉不能停機)、需要異地備份(災難復原)
Sharding(分片)
- 水平分片:按行切分(如用戶 1-1000 在 Shard A,1001-2000 在 Shard B)。每個 Shard 的 Schema 完全相同,只是資料不同。適用判斷:單表行數超過 5,000 萬或單庫容量超過 500GB,且讀寫分離 + Cache 仍撐不住
- 垂直分片:按業務拆分(用戶表在 DB1,訂單表在 DB2,商品表在 DB3)。好處是每個庫只處理自己領域的查詢,缺點是跨庫 JOIN 變得困難。通常在水平分片之前先做垂直分片
- 分片鍵(Shard Key)選擇標準:高基數(Cardinality 至少數萬以上,避免資料傾斜)、查詢頻繁使用(大部分查詢都帶有此欄位,避免跨分片查詢)、分佈均勻(不會某個值特別多)。好的 Shard Key 範例:user_id(UUID)。差的 Shard Key:country_code(只有幾百個值且分佈極不均勻)
- Consistent Hashing:環形 Hash 空間,新增或移除節點時只需搬移約 1/N 的資料(N 為節點數),而傳統
hash % N在節點數變化時幾乎所有資料都要重新分配。實務上搭配 Virtual Node(每個物理節點映射 100-200 個虛擬節點),讓資料分佈更均勻 - 何時該做 Sharding:先確認已經做了讀寫分離、加了 Cache、優化了慢查詢、做了垂直分片,這些都不夠時才考慮。Sharding 的代價:跨分片 JOIN 需要應用層處理、分散式事務複雜度高、資料遷移和再平衡痛苦
分片策略比較
- Range-based:按範圍分(如用戶 ID 1-100 萬在 Shard A、100-200 萬在 Shard B)。優點:範圍查詢快(如「查 ID 50 萬到 60 萬的用戶」只需查一個 Shard)。缺點:容易熱點(新註冊用戶都集中在最後一個 Shard)。適合:時間序列資料(按月份分片,舊資料可歸檔)
- Hash-based:對 Shard Key 做 Hash 再取餘(如
hash(user_id) % 4),分布均勻。優點:不會有熱點。缺點:範圍查詢需要廣播到所有分片(scatter-gather)。適合:用戶資料、訂單資料等需要均勻分佈的場景 - Directory-based:維護一張查找表(Lookup Table)記錄每筆資料在哪個分片。優點:最靈活,可以隨時調整資料分佈。缺點:查找表本身成為瓶頸和 SPOF,每次讀寫多一次查詢。適合:資料分佈規則複雜或需要頻繁調整的場景
常見誤區
⚠️Sharding 是最後手段
Sharding 帶來巨大的複雜度:跨分片 JOIN 困難、事務處理複雜、資料搬移痛苦。在 Sharding 之前,先嘗試:讀寫分離(Replication)、加 Cache、優化查詢、垂直分片。只有這些都不夠時才做水平 Sharding。
設計流程
評估瓶頸
讀取慢?寫入慢?資料量太大?先確定問題
讀寫分離
加 Slave 節點,讀取走 Slave、寫入走 Master
加 Cache 層
熱點資料用 Redis 快取,減少資料庫讀取壓力
垂直分片
把不同業務的表拆分到不同資料庫
水平 Sharding
選擇 Shard Key,用 Consistent Hashing 分配資料
跨分片方案
處理跨分片查詢、分散式事務、資料遷移
程式碼範例
Consistent Hashing(Python 簡化版)
import hashlib
from bisect import bisect_right
class ConsistentHash:
def __init__(self, nodes: list[str], virtual_nodes: int = 150):
self.ring: list[int] = []
self.node_map: dict[int, str] = {}
for node in nodes:
for i in range(virtual_nodes):
key = f"{node}:v{i}"
h = self._hash(key)
self.ring.append(h)
self.node_map[h] = node
self.ring.sort()
def _hash(self, key: str) -> int:
return int(hashlib.md5(key.encode()).hexdigest(), 16)
def get_node(self, data_key: str) -> str:
h = self._hash(data_key)
idx = bisect_right(self.ring, h) % len(self.ring)
return self.node_map[self.ring[idx]]
# 使用
ch = ConsistentHash(["db-shard-1", "db-shard-2", "db-shard-3"])
print(ch.get_node("user:1001")) # -> db-shard-2
print(ch.get_node("user:1002")) # -> db-shard-1讀寫分離設定(C#)
public class DbContextFactory
{
private readonly string _masterConn;
private readonly string[] _slaveConns;
private int _roundRobin = 0;
public DbContextFactory(string master, string[] slaves)
{
_masterConn = master;
_slaveConns = slaves;
}
// 寫入用 Master
public AppDbContext CreateWriteContext()
=> new AppDbContext(_masterConn);
// 讀取用 Slave(Round Robin 分配)
public AppDbContext CreateReadContext()
{
var conn = _slaveConns[Interlocked.Increment(ref _roundRobin)
% _slaveConns.Length];
return new AppDbContext(conn);
}
}架構圖
架構圖解讀:上半部分是讀寫分離架構 — Application 寫入走 Master DB,讀取走 Slave 1/2,Master 透過 Replication 將資料同步到 Slave。下半部分是 Sharding 架構 — Application 根據 user_id 的 Hash 值決定路由到哪個 Shard。實務中通常先部署上半部分(讀寫分離),等 Master 寫入也撐不住或資料量過大時,再加入下半部分(Sharding)。
實戰補充
💡Replication Lag 的實務處理
非同步複製一定有延遲(通常毫秒級,高負載時可達秒級)。實務上的處理方式:
- Read-your-own-writes:用戶剛寫入的資料,短時間內從 Master 讀取
- Monotonic reads:同一用戶的連續讀取固定走同一個 Slave
- Critical reads:對一致性要求高的查詢(如餘額)永遠走 Master
理解測驗
🤔 一個社群平台有 1 億用戶,讀取 QPS 是寫入的 100 倍。第一步應該怎麼擴展資料庫?
🤔 為什麼 Consistent Hashing 在 Sharding 中很重要?
🤔 以下哪個不適合作為 Shard Key?
重點整理
💡一句話記住
讀不夠就複製,裝不下就切片。 記憶口訣:「讀複寫切」— 先讀寫分離 → 加 Cache → 垂直分片 → 最後才水平 Sharding。每一步都比下一步簡單,能不往下走就不走。
| 概念 | 說明 | |------|------| | Master-Slave | 寫入走 Master,讀取走 Slave,最基本的讀寫分離 | | Consistent Hashing | 環形 Hash 空間,節點增減時只搬移少量資料 | | Shard Key | 決定資料落點的欄位,選擇要高基數且均勻分佈 | | Replication Lag | 非同步複製的延遲,需要針對不同場景做處理 | | 垂直 vs 水平分片 | 垂直按欄位拆,水平按行拆,水平擴展性更好 |