在現代網際網路系統架構中,記憶體快取(如 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 同時失效,全站流量砸 DBTTL 隨機抖動、多級快取、降級熔斷

2. 快取穿透(Cache Penetration)與布隆過濾器

2.1 穿透機制

當惡意攻擊者或異常程式發起大量對 id = -1 或不存在的 uuid 的查詢時:

  1. 查詢 Redis 快取:未命中(Key 不存在)。
  2. 查詢 MySQL 資料庫:查無此筆資料(耗時 5~10ms,且無法寫入快取)。
  3. 攻擊者持續以數萬 QPS 併發查詢不同不存在的 Key,導致快取完全形同虛設,所有壓力由資料庫全額承受。

2.2 防禦方案 A:空值快取(Cache Null Values)

當資料庫查無資料時,依然向 Redis 寫入一個空標記(如 {"status": "NOT_FOUND"}),並設定較短的過期時間(如 30~60 秒)。

  • 缺點:若攻擊者每次使用完全隨機且不重複的偽造 Key,Redis 將被無效的空值 Key 填滿,浪費大量記憶體。

2.3 防禦方案 B:布隆過濾器(Bloom Filter)

布隆過濾器是一種空間效率極高、基於位元陣列(Bit Array)與多個獨立雜湊函數的概率型資料結構:

布隆過濾器(Bloom Filter)多雜湊映射與位元陣列架構圖展示輸入 Key 透過 h1、h2、h3 三個獨立雜湊函數映射至 Bit Array 的指定索引位置並置 1。輸入 Key (例如: user_uuid_1092)h1(key)=2h2(key)=5h3(key)=8Bit Array (記憶體極小位元陣列):0010010010
  • 判斷規則:
    • 若布隆過濾器判斷 「不存在」:則該資料在資料庫中 「絕對 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 雪崩機制

  1. 集中過期:開發者在每天凌晨 00:00 執行批次任務,將數十萬個商品的快取 TTL 統一設定為 3600 秒。到了凌晨 01:00:00 整,這數十萬個 Key 在同一秒鐘集體過期,海量查詢同時砸向資料庫。
  2. 快取叢集當機:Redis 主節點發生硬體故障或記憶體 OOM,叢集暫時無法提供服務。

4.2 三重防禦架構體系

  1. TTL 隨機抖動加權(Random Jitter): 在基礎過期時間上增加隨機偏差,將過期時間均勻打散: TTL = BaseTTL + random(1, 300) 秒
  2. 多級快取架構(L1 本地記憶體 + L2 分散式 Redis):
    • L1 快取:服務本地進程內記憶體快取(如 Go ristretto / Java Caffeine),極速且完全無網路 I/O。
    • L2 快取:分散式 Redis 叢集。
    • 即使 Redis 發生短暫抖動,L1 快取仍能為最熱門的資料提供屏障。
  3. 熔斷、限流與主從高可用:
    • 部署 Redis Sentinel 或 Redis Cluster,具備秒級主從自動容錯轉移(Failover)。
    • 在 API 閘道層配置斷路器與限流器(如 Sentinel / Resilience4j),當資料庫負載過高時直接快速失敗或回傳預設降級資料。