在現代高併發分散式架構中,快取(如 Redis、Memcached)與資料庫(如 MySQL、PostgreSQL)往往是協同作戰的黃金搭檔。
然而,許多工程師在處理快取讀寫時,往往面臨經典的一致性困境:
- 寫入資料時,究竟該更新快取還是刪除快取?
- 究竟該先操作資料庫還是先操作快取?
- 網路延遲與高併發交織下,如何防止舊資料覆蓋新資料?
為了解決這些問題,分散式領域總結出了五大經典快取讀寫拓撲模式。本文將全景剖析這五大模式的架構設計、一致性代價與工程最佳實踐。
1. 五大快取讀寫模式全景矩陣
Cache-Aside 旁路快取 (最通用)
Write-Through 直寫模式 (強一致)
Write-Behind 非同步回寫 (極限吞吐)
| 快取模式 | 讀取流程 | 寫入流程 | 應用層感知 | 一致性保證 |
|---|---|---|---|---|
| 1. Cache-Aside | 應用層先查快取, | 應用層先寫 DB, | 應用層負責協調 | 最終一致性 |
| (旁路快取 - 最常用) | 未命中則查 DB 寫入 | 再刪除快取 | 快取與 DB 雙寫 | (可能存在極短延遲) |
| 2. Read-Through | 應用層僅查快取, | 同 Write-Through | 應用層只對接快取 | 最終一致性 |
| (直讀模式) | 快取元件自動載入 DB | 或外部更新 | 快取封裝底層 DB | (依賴快取中介層) |
| 3. Write-Through | 同 Read-Through | 應用層寫快取, | 應用層只對接快取 | 強一致性 |
| (直寫模式) | 快取元件同步寫 DB | 寫入延遲包含 DB | (兩者同步寫入) | |
| 4. Write-Behind | 同 Read-Through | 應用層只寫快取, | 應用層極速寫入 | 最終一致性 |
| (Write-Back 非同步) | 快取非同步批量刷 DB | 存在當機丟資料風險 | (寫入吞吐量極致) | |
| 5. Refresh-Ahead | 應用層查快取, | 外部正常更新 | 自動預測載入 | 適合定期熱點 |
| (預測載入模式) | 快取過期前自動預刷 | 消除快取穿透延遲 | (需精確存取預測) |
2. 深入 Cache-Aside 模式與一致性終極爭議
Cache-Aside 是當今工業界應用最廣泛的模式。其標準步驟如下:
- 讀取(Read):
- 應用程式先查詢快取;若命中則直接返回。
- 若快取未命中,查詢資料庫取得資料。
- 將資料庫讀取到的資料寫入快取,並返回給客戶端。
- 寫入(Write):
- 應用程式先更新資料庫。
- 資料庫更新成功後,刪除快取(Delete Cache)。
2.1 爭議一:為什麼是「刪除快取」而不是「更新快取」?
- 防止併發更新覆蓋(Race Condition):若請求 A 和請求 B 同時發起更新,若採用「更新快取」,由於網路延遲,可能發生「A 先更新 DB -> B 後更新 DB -> B 先更新快取 -> A 後更新快取」,導致快取中殘留了 A 的舊資料!
- 惰性計算節省資源:許多快取值是經過複雜 SQL 聚合計算生成的(如包含多張關聯表)。若每次資料庫變更都去重新計算一次快取,在「寫多讀少」場景下會造成嚴重的 CPU 浪費;採用刪除策略,只有在使用者真正發起讀取時才進行按需計算。
2.2 爭議二:為什麼是「先寫 DB,再刪快取」而不是「先刪快取,再寫 DB」?
若採用 「先刪快取,再寫 DB」,在高併發下極易產生永久髒資料:
- 執行緒 A 刪除快取。
- 執行緒 B 進來發起讀請求,發現快取未命中,去查詢資料庫(此時 A 尚未寫入 DB,B 讀取到舊值)。
- 執行緒 B 將讀到的舊值寫回快取。
- 執行緒 A 終於完成資料庫更新。
- 結果:快取中將永久殘留 B 寫入的舊資料,直到 TTL 過期!
3. 延遲雙刪與 Binlog CDC 非同步淘汰實戰
即便採用了「先寫 DB,再刪快取」,在極端併發讀寫競爭下仍有微小概率產生髒資料(讀未命中 -> 寫 DB -> 刪快取 -> 讀執行緒將舊資料寫入快取)。
3.1 延遲雙刪(Delayed Double Deletion)
func UpdateData(ctx context.Context, key string, data Entity) error {
// 1. 先寫資料庫
if err := db.Update(ctx, data); err != nil {
return err
}
// 2. 第一次刪除快取
redisClient.Del(ctx, key)
// 3. 非同步睡眠一段時間 (例如 500ms,需大於主從同步與讀請求耗時)
go func() {
time.Sleep(500 * time.Millisecond)
// 4. 第二次刪除快取 (徹底清除併發讀線程可能回填的舊快取)
redisClient.Del(context.Background(), key)
}()
return nil
}
3.2 現代黃金標準:Binlog CDC 非同步解耦淘汰
為了消除應用層對刪除快取的維護負擔與延遲雙刪的 sleep 開銷,現代大型架構普遍採用 CDC(Change Data Capture):
Binlog CDC 快取淘汰步驟
應用層僅純粹更新 MySQL 主庫,不承擔快取淘汰責任,徹底消除業務複雜度。
Canal 或 Debezium 模擬 MySQL 備庫協議,即時捕獲 Row 級別變更日誌。
將變更事件投遞至 Kafka / MQ,提供削峰、持久化重試與死信防禦機制。
消費者 Worker 讀取訊息,精準執行 DEL key,並天然適應主從複製延遲。
- 核心優勢:應用層業務邏輯與快取解耦;透過 Kafka 保證刪除指令的可靠重試與死信防禦;天然適配 MySQL 主從延遲。
4. Write-Behind(非同步批量回寫)的極限效能
在需要極致寫入吞吐量(例如遊戲伺服器角色經驗值、文章即時觀看數、點讚數)的系統中,每次點讚都去打一次資料庫會瞬間讓資料庫連線池崩潰。
- 運作機制:應用層所有的讀寫操作完全只在 Redis 記憶體中進行。
- 背景批次刷盤:背景守護行程以固定間隔(如每 5 秒)或定量累積(如滿 1000 筆)將記憶體中的變更合併為單一批量 SQL(
INSERT ... ON DUPLICATE KEY UPDATE)一次性回寫資料庫。 - 代價權衡:將資料庫寫入壓力降低了 99%,但若 Redis 伺服器在刷盤前發生硬體掉電,會丟失過去 5 秒內的記憶體資料。
5. 架構選型總結
- 通用 Web 業務、電商交易、帳戶資料:一律選用 Cache-Aside + Binlog CDC 非同步淘汰。
- 高頻計數、即時互動、允許極微量丟失:採用 Write-Behind(非同步回寫)。
- 熱點新聞、定期報表、防穿透:採用 Refresh-Ahead 預測載入。
