什么是緩存?
緩存(Cache)是一種將熱點數據緩存在內存中(如 Redis)以加快訪問速度、減輕數據庫壓力的技術。
但引入緩存后可能出現 三大核心問題:
- 緩存穿透(Cache Penetration)
- 緩存擊穿(Cache Breakdown)
- 緩存雪崩(Cache Avalanche)
一、緩存穿透(Cache Penetration)
問題描述
緩存穿透指:請求的數據既不在緩存中,也不在數據庫中,導致請求每次都打到數據庫。
常見場景
- 惡意攻擊:傳入大量隨機 ID,繞過緩存層直擊數據庫
- 用戶訪問非法 ID,如
/user?id=-1
舉例
用戶頻繁訪問一個 不存在的商品 ID:99999999:
- Redis 無此數據 → 查詢數據庫
- 數據庫無 → 返回 null
- 下次再次請求 ID=99999999,又重復上述過程 → DB 被壓垮
解決方案
1. 緩存空值
if (dbData == null) {redis.set("shop:99999999", "", 2分鐘);
}
- 空值也緩存,避免重復查數據庫
- 設較短 TTL(避免緩存過期數據太久)
2. 參數校驗攔截非法請求
- 如:ID 不能為負數或超過最大值
- 在請求層面做過濾,不進 DB 或 Redis
3. 布隆過濾器(適用于大數據量)
- 將所有合法 ID 加入布隆過濾器
- 請求前先判斷是否命中布隆過濾器,不在則直接拒絕
二、緩存擊穿(Cache Breakdown)
問題描述
緩存擊穿指:某個熱點 Key 剛好失效時,大量并發請求打到數據庫,導致數據庫瞬時壓力激增。
常見場景
- 熱點數據正好在高峰期過期
- 比如:商品詳情頁、秒殺商品、搶購庫存
舉例
商品 ID=1
每天百萬訪問量,緩存過期瞬間,大量用戶同時訪問導致:
- Redis 查不到 → 并發查詢 DB → 數據庫壓力飆升
解決方案
1. 互斥鎖方式:單線程緩存重建
if (redis.get("shop:1") == null) {if (tryLock("lock:shop:1")) {// 從 DB 讀取 → 緩存寫回 Redisunlock();} else {// 其他線程等待或返回默認值}
}
- 緩存重建交給首個拿到鎖的線程,其它線程等待或快速失敗
2. 邏輯過期 + 異步重建(推薦)
{"data": {...},"expireTime": "2025-06-30 12:00:00"
}
- 緩存提前設置一個邏輯過期時間(保存在 value 中)
- 判斷已過期 → 異步線程后臺刷新 → 返回舊數據不中斷用戶體驗
適合熱點數據緩存更新
三、緩存雪崩(Cache Avalanche)
問題描述
大量緩存同時過期,導致所有請求同時訪問數據庫,引發系統雪崩。
常見場景
- 設置了相同 TTL 的大量緩存同時過期
- Redis 重啟或崩潰,緩存瞬間全部丟失
舉例
- 秒殺系統中 10 萬商品都設置
TTL=24小時
- 恰好第二天凌晨失效 → 所有請求打到數據庫
解決方案
1. 緩存過期時間加隨機
int ttl = 3600 + RandomUtil.randomInt(0, 600);
redis.set("shop:" + id, value, ttl, TimeUnit.SECONDS);
- 避免所有 key 同一時間過期,均勻錯開時間點
2. 熱點數據永不過期 + 后臺異步刷新
- 邏輯過期方案 + 后臺定時更新
- 熱點數據維持高可用
3. 多級緩存(本地 + 分布式)
- 如:Caffeine + Redis + MySQL 三層緩存
- Redis 崩潰時,先從本地緩存兜底
4. 限流+降級
- 接口層加限流、熔斷、降級返回默認值,避免雪崩擴大化
項目中 Redis 緩存策略總結
問題 | 定義 | 解決方案 |
---|---|---|
緩存穿透 | 請求數據既不在緩存也不在數據庫 | 緩存空值、參數校驗、布隆過濾器 |
緩存擊穿 | 熱點 key 在高并發下剛好失效 | 加鎖互斥、邏輯過期 + 異步刷新 |
緩存雪崩 | 大量 key 同時過期、或 Redis 故障 | 加 TTL 隨機值、熱點永不過期、多級緩存、限流降級 |
實戰建議
- 所有緩存數據 務必設置 TTL,默認不要永久存在
- 區分冷數據(短 TTL)與熱點數據(長 TTL 或邏輯過期)
- 高并發業務使用異步線程池或消息隊列緩沖請求
- 建立統一的緩存封裝組件(CacheClient),集中處理這些問題