效能設計的目標是提升系統的吞吐量與回應時間,主要策略包括快取、非同步處理、讀寫分離與資料分片。
快取設計 (Caching)#
快取是提高效能最有效的方式之一,通過將熱點資料放在記憶體中,減少對資料庫的存取。
快取是通過犧牲強一致性來提高效能的。並非所有業務都適合使用快取,需要根據業務需求評估資料的即時性要求。
Cache Aside 模式#
最常用的快取更新模式(Facebook 採用此策略):
讀取流程:
1. 從快取讀取
2. 快取命中 → 回傳資料
3. 快取未命中 → 從資料庫讀取 → 存入快取 → 回傳資料
更新流程:
1. 更新資料庫
2. 刪除快取(不是更新快取!)為什麼是刪除快取而不是更新快取?
避免兩個並行寫操作導致髒資料:
時間線:
T1: 操作 A 更新資料庫為 valueA
T2: 操作 B 更新資料庫為 valueB
T3: 操作 B 更新快取為 valueB
T4: 操作 A 更新快取為 valueA ← 髒資料!
結果:資料庫是 valueB,快取是 valueACache 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 策略,這也是為什麼非正常關機可能導致資料丟失。
快取設計要點#
- 命中率:80% 以上算良好,追求效能可到 95%
- 過期時間:太短會頻繁穿透,太長會浪費記憶體
- LRU 策略:淘汰最久未使用的資料(注意加鎖開銷)
- 防爬蟲:避免爬蟲爬到冷門資料,擠掉熱點資料
- 分散式快取:使用 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:#fff9c4Broker 模式#
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 | 需要均勻分布的場景 |
強烈建議從業務角度分片,不要使用雜湊散列分片!
雜湊分片的問題:
- 跨分片查詢困難
- 跨分片事務複雜
- 擴容需要重新雜湊
分片的挑戰#
- 跨分片查詢:需要查詢多個分片後合併結果
- 跨分片事務:需要使用分散式事務(2PC/TCC)
- 資料傾斜:某些分片資料過多,需要定期重新平衡
- 分片路由:需要資料訪問層中間件解析 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效能最佳化的原則:
- 先測量,再最佳化(沒有資料的最佳化是盲目的)
- 優先最佳化瓶頸點
- 在一致性和效能之間做取捨
- 能用快取解決的問題,不要用分片