在現代網際網路系統架構中,記憶體快取(如 Redis、Memcached)是承受海量讀取流量(QPS 數十萬至數百萬)的第一道堅固盾牌。透過將熱點資料保留在記憶體中,快取將絕大部分讀請求直接在微秒級(μs)內攔截,保護後端脆弱的關聯式資料庫(如 MySQL、PostgreSQL)。
然而,當快取層發生異常或失效時,原本被快取抵擋的海量流量會瞬間直接砸向底層資料庫,造成資料庫 CPU 飆升至 100%、連線池耗盡、乃至整個後端服務群體崩潰。
這就是分散式快取體系中最經典的三大災難:快取穿透(Cache Penetration)、快取擊穿(Cache Breakdown / Hotspot Invalidation) 與 快取雪崩(Cache Avalanche)。
本文將從發生機制、受害鏈條到生產級防禦程式碼,深度剖析這三大問題的本質與完整解法。
1. 三大快取災難核心定義與特徵矩陣
| 災難類型 | 根本誘發原因 | 流量特徵 | 核心解法 |
|---|---|---|---|
| 1. 快取穿透 | 查詢【根本不存在】的資料 | 大量惡意或無效 Key,持續穿透至 DB | 布隆過濾器 (Bloom Filter)、空值快取 |
| 2. 快取擊穿 | 【單一超高熱點 Key】在某瞬間過期 | 單一 Key 瞬間湧入數萬併發直接打 DB | 分散式互斥鎖 (Mutex)、邏輯非同步過期 |
| 3. 快取雪崩 | 【海量 Key 同時過期】或快取節點當機 | 大量不同 Key 同時失效,全站流量砸 DB | TTL 隨機抖動、多級快取、降級熔斷 |
2. 快取穿透(Cache Penetration)與布隆過濾器
2.1 穿透機制
當惡意攻擊者或異常程式發起大量對 id = -1 或不存在的 uuid 的查詢時:
- 查詢 Redis 快取:未命中(Key 不存在)。
- 查詢 MySQL 資料庫:查無此筆資料(耗時 5~10ms,且無法寫入快取)。
- 攻擊者持續以數萬 QPS 併發查詢不同不存在的 Key,導致快取完全形同虛設,所有壓力由資料庫全額承受。
2.2 防禦方案 A:空值快取(Cache Null Values)
當資料庫查無資料時,依然向 Redis 寫入一個空標記(如 {"status": "NOT_FOUND"}),並設定較短的過期時間(如 30~60 秒)。
- 缺點:若攻擊者每次使用完全隨機且不重複的偽造 Key,Redis 將被無效的空值 Key 填滿,浪費大量記憶體。
2.3 防禦方案 B:布隆過濾器(Bloom Filter)
布隆過濾器是一種空間效率極高、基於位元陣列(Bit Array)與多個獨立雜湊函數的概率型資料結構:
- 判斷規則:
- 若布隆過濾器判斷 「不存在」:則該資料在資料庫中 「絕對 100% 不存在」,直接攔截並返回,完全不碰資料庫!
- 若布隆過濾器判斷 「存在」:可能存在極低機率的誤判(False Positive,通常配置在 0.01% 以下),此時才放行請求查快取或資料庫。
- 實踐:使用 Redis 內建的 RedisBloom 模組,在寫入新資料時同步將 Key 加入過濾器中。
3. 快取擊穿(Cache Breakdown)與互斥重建
3.1 擊穿機制
假設某秒殺商品 product:1001 是全站頂級熱點(每秒 5 萬次讀取)。當該 Key 的 TTL 到期並在 12:00:00.000 瞬間被 Redis 刪除時:
- 在
12:00:00.001這 1 毫秒內湧入的 5000 個併發執行緒,全部發現快取未命中。 - 這 5000 個執行緒同時向 MySQL 發起
SELECT * FROM products WHERE id = 1001,導致資料庫瞬間被擠爆。
3.2 解法 A:分散式互斥鎖(Distributed Mutex Lock)
只有搶到鎖的單一執行緒有權去查詢資料庫並重建快取,其餘執行緒原地睡眠重試:
func GetData(ctx context.Context, key string) (string, error) {
// 1. 查快取
val, err := redisClient.Get(ctx, key).Result()
if err == nil {
return val, nil
}
// 2. 快取未命中,嘗試搶互斥鎖
lockKey := "lock:" + key
acquired := redisClient.SetNX(ctx, lockKey, "1", 10*time.Second).Val()
if acquired {
defer redisClient.Del(ctx, lockKey)
// 3. 雙重檢查 (Double-Check),防其他執行緒已重建完畢
val, err = redisClient.Get(ctx, key).Result()
if err == nil {
return val, nil
}
// 4. 查 DB 並寫回快取
val = queryFromDB(key)
redisClient.Set(ctx, key, val, 30*time.Minute)
return val, nil
} else {
// 5. 沒搶到鎖,睡眠 50ms 後重試
time.Sleep(50 * time.Millisecond)
return GetData(ctx, key)
}
}
3.3 解法 B:邏輯過期(Logical Expiration)
- 在 Redis 中儲存的 Value 物件內部封裝一個
expire_at時間戳,但 Redis 本身的 Key 永不過期(No TTL)。 - 當讀取到資料並發現
now > expire_at時:- 立即將舊資料(Stale Data)直接返回給使用者(保證即時回應)。
- 同時在背景非同步拋出一個 Goroutine / 執行緒去搶鎖並更新資料庫資料與
expire_at。
4. 快取雪崩(Cache Avalanche)與全鏈路防禦
4.1 雪崩機制
- 集中過期:開發者在每天凌晨 00:00 執行批次任務,將數十萬個商品的快取 TTL 統一設定為
3600 秒。到了凌晨 01:00:00 整,這數十萬個 Key 在同一秒鐘集體過期,海量查詢同時砸向資料庫。 - 快取叢集當機:Redis 主節點發生硬體故障或記憶體 OOM,叢集暫時無法提供服務。
4.2 三重防禦架構體系
- TTL 隨機抖動加權(Random Jitter):
在基礎過期時間上增加隨機偏差,將過期時間均勻打散:
TTL = BaseTTL + random(1, 300) 秒 - 多級快取架構(L1 本地記憶體 + L2 分散式 Redis):
- L1 快取:服務本地進程內記憶體快取(如 Go
ristretto/ JavaCaffeine),極速且完全無網路 I/O。 - L2 快取:分散式 Redis 叢集。
- 即使 Redis 發生短暫抖動,L1 快取仍能為最熱門的資料提供屏障。
- L1 快取:服務本地進程內記憶體快取(如 Go
- 熔斷、限流與主從高可用:
- 部署 Redis Sentinel 或 Redis Cluster,具備秒級主從自動容錯轉移(Failover)。
- 在 API 閘道層配置斷路器與限流器(如 Sentinel / Resilience4j),當資料庫負載過高時直接快速失敗或回傳預設降級資料。
