資料庫擴展(Database Scaling)

是什麼?

當單一資料庫撐不住流量或資料量時,需要透過 Replication 和 Sharding 來擴展。這是 System Design 中最關鍵也最複雜的環節。

ℹ️兩種擴展方向

Replication = 同一份資料複製多份,提高讀取能力和可用性。 Sharding = 把資料切分到多台機器,提高寫入能力和儲存容量。

核心觀念

Replication(複製)

Sharding(分片)

分片策略比較

常見誤區

⚠️Sharding 是最後手段

Sharding 帶來巨大的複雜度:跨分片 JOIN 困難、事務處理複雜、資料搬移痛苦。在 Sharding 之前,先嘗試:讀寫分離(Replication)、加 Cache、優化查詢、垂直分片。只有這些都不夠時才做水平 Sharding。

設計流程

1

評估瓶頸

讀取慢?寫入慢?資料量太大?先確定問題

2

讀寫分離

加 Slave 節點,讀取走 Slave、寫入走 Master

3

加 Cache 層

熱點資料用 Redis 快取,減少資料庫讀取壓力

4

垂直分片

把不同業務的表拆分到不同資料庫

5

水平 Sharding

選擇 Shard Key,用 Consistent Hashing 分配資料

6

跨分片方案

處理跨分片查詢、分散式事務、資料遷移

程式碼範例

Consistent Hashing(Python 簡化版)

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#)

csharp
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
write
Master DB (Write)
replication
Slave 1 (Read)
Slave 2 (Read)
Shard A (User 1-1M)
Shard B (User 1M-2M)
Shard C (User 2M+)

架構圖解讀:上半部分是讀寫分離架構 — Application 寫入走 Master DB,讀取走 Slave 1/2,Master 透過 Replication 將資料同步到 Slave。下半部分是 Sharding 架構 — Application 根據 user_id 的 Hash 值決定路由到哪個 Shard。實務中通常先部署上半部分(讀寫分離),等 Master 寫入也撐不住或資料量過大時,再加入下半部分(Sharding)。

實戰補充

💡Replication Lag 的實務處理

非同步複製一定有延遲(通常毫秒級,高負載時可達秒級)。實務上的處理方式:

  1. Read-your-own-writes:用戶剛寫入的資料,短時間內從 Master 讀取
  2. Monotonic reads:同一用戶的連續讀取固定走同一個 Slave
  3. 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 水平分片 | 垂直按欄位拆,水平按行拆,水平擴展性更好 |

你可能也想看

核心元件設計短網址服務

按 ← → 鍵切換課程