效能設計的目標是提升系統的吞吐量與回應時間,主要策略包括快取、非同步處理、讀寫分離與資料分片。

快取設計 (Caching)#

快取是提高效能最有效的方式之一,通過將熱點資料放在記憶體中,減少對資料庫的存取。

快取是通過犧牲強一致性來提高效能的。並非所有業務都適合使用快取,需要根據業務需求評估資料的即時性要求。

Cache Aside 模式#

最常用的快取更新模式(Facebook 採用此策略):

讀取流程:
  1. 從快取讀取
  2. 快取命中 → 回傳資料
  3. 快取未命中 → 從資料庫讀取 → 存入快取 → 回傳資料

更新流程:
  1. 更新資料庫
  2. 刪除快取(不是更新快取!)

為什麼是刪除快取而不是更新快取?

避免兩個並行寫操作導致髒資料:

時間線:
T1: 操作 A 更新資料庫為 valueA
T2: 操作 B 更新資料庫為 valueB
T3: 操作 B 更新快取為 valueB
T4: 操作 A 更新快取為 valueA  ← 髒資料!

結果:資料庫是 valueB,快取是 valueA
Cache Aside 的並行問題

理論上,Cache Aside 也可能出現髒資料:

T1: 讀操作發現快取失效
T2: 讀操作從資料庫讀取舊資料
T3: 寫操作更新資料庫
T4: 寫操作刪除快取
T5: 讀操作把舊資料放入快取  ← 髒資料!

但這種情況發生的機率很低,因為:

  • 需要讀操作先進入資料庫
  • 讀操作還要晚於寫操作更新快取
  • 寫操作比讀操作慢得多,還要加鎖

解決方案

  • 使用 2PC 或 Paxos(太慢或太複雜)
  • 設置快取過期時間(簡單有效)

Read/Write Through 模式#

應用程式只與快取互動,快取負責與資料庫同步:

flowchart TD
    A[應用程式] -->|讀/寫| B[快取]
    B -->|同步| C[資料庫]
    B -.->|負責與資料庫同步| B

    style A fill:#e3f2fd
    style B fill:#fff9c4
    style C fill:#c8e6c9
  • Read Through:快取失效時,由快取服務負責載入資料
  • Write Through:寫入時,由快取服務同步寫入資料庫

Write Behind (Write Back) 模式#

只更新快取,非同步批量寫入資料庫:

寫入流程:
  1. 更新快取
  2. 標記為 dirty
  3. 非同步批量寫入資料庫

優點:
  - 極高的寫入效能
  - 可合併多次寫操作

缺點:
  - 資料可能丟失(如系統崩潰)
  - 不保證強一致性

Linux 的 Page Cache 就是使用 Write Back 策略,這也是為什麼非正常關機可能導致資料丟失。

快取設計要點#

  1. 命中率:80% 以上算良好,追求效能可到 95%
  2. 過期時間:太短會頻繁穿透,太長會浪費記憶體
  3. LRU 策略:淘汰最久未使用的資料(注意加鎖開銷)
  4. 防爬蟲:避免爬蟲爬到冷門資料,擠掉熱點資料
  5. 分散式快取:使用 Redis 集群,避免本地快取的一致性問題

非同步處理 (Asynchronous)#

非同步處理可以提高系統吞吐量,讓資源得到更好的利用。

同步 vs 非同步#

特性同步非同步
吞吐量受最慢環節限制可並行處理
資源利用等待時資源閒置資源利用率高
一致性即時一致最終一致
複雜度簡單較複雜

非同步通訊方式#

請求回應式#

sequenceDiagram
    participant S as 發送方
    participant R as 接收方
    S->>R: 請求
    R-->>S: 回呼

問題:發送方需要提供回呼 URL,仍有一定耦合。

訂閱式 (Pub/Sub)#

flowchart LR
    S[發送方] -->|發布事件| Q[訊息佇列]
    Q -->|訂閱| R[接收方]

    style Q fill:#fff9c4

Broker 模式#

flowchart LR
    A[服務 A] --> B[Broker]
    A2[服務 B] --> B
    B --> C[服務 C]
    B --> D[服務 D]

    style B fill:#ffccbc

完全解耦,所有服務只依賴 Broker。

事件驅動架構 (EDA)#

flowchart TD
    O[下單服務] -->|下單事件| Q[訊息佇列]
    Q --> S1[訂單服務]
    Q --> S2[庫存服務]
    Q --> S3[支付服務]
    S1 --> R1[生成訂單]
    S2 --> R2[佔住庫存]
    S3 --> R3[處理支付]

    style Q fill:#fff9c4
    style R1 fill:#c8e6c9
    style R2 fill:#c8e6c9
    style R3 fill:#c8e6c9

優點

  • 服務間無依賴,高度可重用
  • 開發、測試、運維隔離
  • 不會相互阻塞
  • 便於增加中間處理(日誌、認證、限流)

事件溯源 (Event Sourcing)#

只記錄不可變的事件,通過回放事件得到最終狀態:

傳統方式:
  餘額 = 1000(狀態)

事件溯源:
  +500 ──▶ +200 ──▶ -100 ──▶ +400 = 1000
  (事件流)

優點:
  - 完整的歷史記錄
  - 可以重新計算狀態(修復 bug 後重放)
  - 無並行衝突

讀寫分離 (CQRS)#

基本讀寫分離#

flowchart TD
    S[服務層] -->|寫| M[(主庫)]
    S -->|讀| R[(從庫)]
    M -->|同步| R

    style S fill:#e3f2fd
    style M fill:#ffccbc
    style R fill:#c8e6c9

優點

  • 簡單易實現
  • 業務隔離良好
  • 分擔讀負載

缺點

  • 主庫單點問題
  • 同步延遲,不適合強一致性場景

CQRS (Command Query Responsibility Segregation)#

命令 (Command):
  - 寫操作(增、刪、改)
  - 不回傳資料,只回傳執行狀態
  - 會改變系統狀態

查詢 (Query):
  - 讀操作
  - 回傳資料
  - 不改變系統狀態

CQRS + Event Sourcing

flowchart LR
    subgraph Write["命令端 (寫模型)"]
        W[寫入操作]
    end
    subgraph Read["查詢端 (讀模型)"]
        R[讀取操作]
    end
    W --> ES[(事件存儲)]
    ES -->|投影| RV[(讀取視圖)]
    RV --> R

    style Write fill:#ffccbc
    style Read fill:#c8e6c9
    style ES fill:#fff9c4
    style RV fill:#e3f2fd

資料分片 (Sharding)#

當單一資料庫無法承載資料量時,需要進行分庫分表。

分片策略#

策略說明適用場景
按租戶用租戶 ID 分片多租戶 SaaS 系統
按類型按商品類目、地區等分片電商平台
按範圍按時間、ID 範圍分片訂單系統(按月分表)
按雜湊ID % N需要均勻分布的場景

強烈建議從業務角度分片,不要使用雜湊散列分片!

雜湊分片的問題:

  • 跨分片查詢困難
  • 跨分片事務複雜
  • 擴容需要重新雜湊

分片的挑戰#

  1. 跨分片查詢:需要查詢多個分片後合併結果
  2. 跨分片事務:需要使用分散式事務(2PC/TCC)
  3. 資料傾斜:某些分片資料過多,需要定期重新平衡
  4. 分片路由:需要資料訪問層中間件解析 SQL 並路由
分片路由的實現
flowchart TB
    subgraph DAL["資料訪問層"]
        S1["1. 解析 SQL"]
        S2["2. 根據分片鍵路由到對應分片"]
        S3["3. 合併多分片結果"]
        S1 --> S2 --> S3
    end

    DAL --> D1[(分片 1)]
    DAL --> D2[(分片 2)]
    DAL --> D3[(分片 3)]

    style DAL fill:#e3f2fd
    style D1 fill:#c8e6c9
    style D2 fill:#c8e6c9
    style D3 fill:#c8e6c9

對於分頁、聚合等操作,需要:

  • 到各分片分別查詢
  • 在資料訪問層合併結果

垂直分片#

將表的欄位拆分到不同的表:

原表:products (id, name, description, price, stock)

垂直分片後:
  products_info (id, name, description)  ← 不常修改
  products_stock (id, price, stock)      ← 經常修改

優點:避免修改某欄位時鎖住不相關的欄位

負載均衡策略#

策略說明
輪詢 (Round Robin)依序分配請求
加權輪詢根據伺服器效能分配不同權重
最少連線分配給當前連線數最少的伺服器
IP Hash相同 IP 分配到同一伺服器(工作階段保持)
一致性雜湊減少擴縮容時的資料遷移

效能設計總結#

flowchart TB
    subgraph App["應用層"]
        A1["快取(Redis)"]
        A2["非同步處理(訊息佇列)"]
        A3["連線池"]
    end

    subgraph Data["資料層"]
        D1["讀寫分離"]
        D2["分庫分表"]
        D3["索引最佳化"]
    end

    subgraph Infra["基礎設施"]
        I1["負載均衡"]
        I2["CDN"]
        I3["壓縮 / 合併"]
    end

    App --> Data --> Infra

    style App fill:#e3f2fd
    style Data fill:#fff9c4
    style Infra fill:#c8e6c9

效能最佳化的原則:

  1. 先測量,再最佳化(沒有資料的最佳化是盲目的)
  2. 優先最佳化瓶頸點
  3. 在一致性和效能之間做取捨
  4. 能用快取解決的問題,不要用分片