緩存擊穿完整講解 + 防御方案
一、什么是緩存擊穿(Cache Breakdown)?
定義:
某個「熱點Key」非常重要,訪問量極高;
當它 剛好過期的那一刻,大量請求同時訪問該Key,由于緩存失效,會 瞬間全部打到數據庫,導致數據庫壓力驟增,甚至宕機。
二、緩存擊穿與其他問題的區別
| 問題類型 | 觸發條件 | 表現 | 解決方向 |
|---|---|---|---|
| 緩存穿透 | 請求不存在的數據(數據庫也無) | 每次都查DB | 加布隆過濾器或空值緩存 |
| 緩存擊穿 | 熱點Key過期 | 一瞬間DB被打爆 | 加鎖、熱點Key永不過期 |
| 緩存雪崩 | 大量Key同時過期 | DB壓力突增 | 隨機過期時間 + 限流 |
三、緩存擊穿的防御方案
方案一:互斥鎖(Mutex Lock)
思路:
當緩存失效時,只有一個請求能去查數據庫,其余請求等待。
適合「高并發下的熱點Key」。
實現示意(偽代碼):
String value = redis.get("product:1001");
if (value == null) {
// 嘗試獲取分布式鎖
if (tryLock("lock:product:1001")) {
// double check 避免重復查詢
value = redis.get("product:1001");
if (value == null) {
value = db.query("SELECT * FROM product WHERE id=1001");
redis.set("product:1001", value, 60);
}
unlock("lock:product:1001");
} else {
// 其他線程短暫休眠后重試
Thread.sleep(50);
value = redis.get("product:1001");
}
}
return value;
常用命令:
SET lock:product:1001 1 NX EX 5
NX:僅當鎖不存在時設置 EX:自動過期防死鎖方案二:熱點 Key 永不過期 + 后臺異步更新
思路:
對極熱點數據(如排行榜、商品詳情)——不要讓它自然過期,而是定時刷新。
做法:
- 設置 Key 永不過期;
- 使用定時任務或消息隊列定期更新內容;
- 或在請求線程中異步刷新:
// 異步刷新策略
if (System.currentTimeMillis() - cache.getUpdateTime("hotKey") > 10分鐘) {
threadPool.submit(() -> refreshHotKeyFromDB());
}
優點:
- 不會發生“過期瞬間擊穿”
- 適合讀多寫少、穩定熱點數據
方案三:邏輯過期(雙層時間機制)
思路:
緩存中設置一個邏輯過期時間,不立即刪除數據,而是由后臺線程更新。
{
"data": {...},
"expireTime": "2025-10-28 14:00:00"
}
邏輯:
if (now < expireTime) {
return cache.data; // 直接返回舊值
} else {
// 異步線程去更新DB + 緩存
refreshAsync();
return cache.data; // 先返回舊數據,保證服務穩定
}
優點:
- 用戶始終有數據返回,不會訪問DB暴增
- 類似“后臺熱更新”
方案四:多級緩存(本地 + Redis)
在應用層增加一層 本地緩存(如 Caffeine/Guava),
Redis 過期時也能頂一會兒,進一步防止瞬間擊穿。
四、實際項目最佳實踐推薦
| 場景 | 推薦方案 |
|---|---|
| 普通熱點Key(商品詳情) | 互斥鎖 + 雙查機制 |
| 極高熱度Key(首頁配置、排行榜) | 永不過期 + 定時刷新 |
| 高QPS系統(上億訪問) | 邏輯過期 + 異步刷新 + 本地緩存 |
| 分布式環境 | 使用 Redisson 或 Redis 原生鎖 |

