Redis之緩存穿透
文章目錄
- Redis之緩存穿透
- 一、什么是緩存穿透?
- 二、緩存穿透常見的解決方案
- 1. 緩存空對象(Null Caching)
- 2. 布隆過濾器(Bloom Filter)?
- 3. 互斥鎖(Mutex Lock)?
- 4. 接口層校驗
- 5. 熱點數據永不過期
- 6. 緩存預熱
- 7. 實時監控與限流
- ☆☆☆組合方案推薦☆☆☆
- 四、實踐:緩存空對象
一、什么是緩存穿透?
- 緩存穿透的定義:緩存穿透(Cache Penetration)是指客戶端請求的數據在緩存中和數據庫中都不存在,這樣緩存永遠不會生效,這些請求都會打到數據庫,導致數據庫壓力驟增甚至崩潰。
- 觸發原因:惡意攻擊、參數偽造、業務邏輯漏洞。
- 核心問題:大量請求訪問數據庫中不存在的數據,緩存無法攔截。
- 示例場景:攻擊者發送大量隨機ID查詢商品信息,這些ID在數據庫中不存在,導致每次請求都穿透緩存直達數據庫。
-
與緩存擊穿的區別
- 緩存擊穿的定義:緩存擊穿(Cache Breakdown)?是指某個熱點key?(如爆款商品信息)在緩存中過期后,大量并發請求同時訪問數據庫(請求數據存在),導致數據庫壓力驟增。
- ?觸發原因:緩存過期時間到期,且高并發場景下請求集中失效。
- 核心問題:單個熱點key失效后,大量請求同時訪問數據庫。
- 示例場景:某明星商品突然爆火,緩存中存儲的商品信息過期后,所有用戶請求同時涌入數據庫查詢。
維度 緩存穿透 緩存擊穿 觸發原因 請求不存在的數據 熱點key過期后高并發請求 數據合法性 數據本身不存在(非法參數) 數據存在但緩存失效(合法參數) 攻擊性 可能是惡意攻擊 正常業務高并發 影響范圍 分散的無效請求 集中在某個熱點key 解決方案 布隆過濾器、緩存空對象 互斥鎖、永不過期、后臺更新
二、緩存穿透常見的解決方案
1. 緩存空對象(Null Caching)
- 原理: 當查詢數據庫發現數據不存在時,將空結果(如
null
)寫入緩存,并設置較短的過期時間。 - 優點:簡單易實現,直接攔截后續相同請求。
- 缺點:
- 內存浪費(存儲大量無效
null
值)。 - 可能出現短時不一致,如:數據已補錄,但緩存未及時失效。(如需強一致性,可以在更新數據時,刪除/覆蓋緩存)
- 內存浪費(存儲大量無效
- 實現:
public Object getData(String key) {// 1. 查詢緩存Object data = cache.get(key);if (data != null) return data;// 2. 查詢數據庫data = db.query(key);if (data == null) {// 緩存空對象,設置短期過期時間(如5分鐘)cache.set(key, "NULL", 5 * 60);} else // 正常數據設置較長過期時間cache.set(key, data, 60 * 60);}return data; }
2. 布隆過濾器(Bloom Filter)?
- 原理:?在緩存層前加布隆過濾器,預先存儲所有合法 Key 的哈希值。查詢時先檢查布隆過濾器:
- 若返回“不存在”,直接攔截請求。
- 若返回“可能存在”,繼續查詢緩存/數據庫。
- 優點:內存占用低(沒有多余的Key),適合海量數據;查詢時間復雜度 O(1)。
- 缺點:
- 存在誤的可能(可能將不存在判斷為存在)。
- 實現復雜,刪除元素困難(需重建過濾器)。
- 適用場景:數據量大且允許誤判(如黑名單校驗)。
- 實現:
// 初始化布隆過濾器(偽代碼) BloomFilter bloomFilter = BloomFilter.create(Funnels.stringFunnel(), expectedInsertions);// 數據預熱時加載存在的key db.keys().forEach(k -> bloomFilter.put(k));public Object getData(String key) {// 1. 先查布隆過濾器if (!bloomFilter.mightContain(key)) {return null; // 直接攔截不存在的key}// 2. 查詢緩存/數據庫Object data = cache.get(key);if (data == null) {data = db.query(key);cache.set(key, data);}return data; }
3. 互斥鎖(Mutex Lock)?
- 原理:緩存未命中時,通過互斥鎖(如 Redis 的
SETNX
)保證只有一個線程查詢數據庫,其他線程等待回填緩存。 - 優點:避免大量請求同時穿透到數據庫。
- 缺點:
- 分布式環境下需使用分布式鎖(如 Redis RedLock)。
- 鎖競爭可能成為性能瓶頸。
- 實現:
public Object getData(String key) {Object data = cache.get(key);if (data != null) return data;// 加鎖(如Redis的SETNX)String lockKey = "lock:" + key;if (redis.setnx(lockKey, "1", 10)) { // 10秒鎖超時try {// 二次檢查緩存(防止鎖競爭期間其他線程已加載)data = cache.get(key);if (data != null) return data;data = db.query(key);cache.set(key, data);} finally {redis.del(lockKey); // 釋放鎖}} else {// 等待重試Thread.sleep(100);return getData(key);}return data; }
4. 接口層校驗
- 原理:在 API 入口處校驗參數合法性,攔截明顯無效的請求(如非法 ID 格式、負數等)。
- ?優點:低成本防御惡意攻擊(如掃描全表 ID)。
- 缺點:無法攔截合法參數但實際不存在的數據請求。
- 示例:校驗 ID 是否為正整數、長度是否符合預期。
- 實現:
public ResponseEntity<?> handleRequest(@PathVariable String id) {if (!isValidId(id)) { // 校驗ID合法性return ResponseEntity.badRequest().build();}// 繼續處理業務邏輯 }
5. 熱點數據永不過期
- 原理:對高頻訪問的熱點數據設置永不過期,通過后臺線程主動更新緩存。
- 優點:徹底避免緩存失效導致的穿透。
- 缺點:數據一致性依賴更新機制,需處理臟數據問題。
- 實現:結合定時任務或事件驅動更新緩存。
// 緩存寫入時設置永不過期 cache.set("hot_key", data);// 后臺定時任務刷新數據 @Scheduled(fixedRate = 300000) void refreshHotData() {Object newData = db.query("hot_key");cache.set("hot_key", newData); }
6. 緩存預熱
- 原理:在系統啟動或低峰期,預先加載熱點數據到緩存中。
- ?優點:減少冷啟動時的緩存穿透風險。
- ?缺點:需提前知道熱點數據(可通過歷史日志分析)。
7. 實時監控與限流
- 原理:監控異常流量(如大量
null
響應),觸發限流策略(如令牌桶、漏桶算法),保護數據庫。 - ?優點:兜底防御,避免突發攻擊。
- ?缺點:需配套監控和告警系統。
☆☆☆組合方案推薦☆☆☆
- 常規場景:緩存空對象 + 接口參數校驗。
- 海量數據:布隆過濾器 + 緩存空對象。
- 高并發熱點數據:永不過期緩存 + 后臺更新線程 + 互斥鎖。
四、實踐:緩存空對象
解決根據id查詢商鋪信息過程中的緩存穿透
@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result queryById(Long id) {// 1.從redis查詢商鋪緩存String key = CACHE_SHOP_KEY + id;String shopJson = stringRedisTemplate.opsForValue().get(key);// 2.判斷是否存在if (StrUtil.isNotBlank(shopJson)) {// 3.存在,直接返回Shop shop = JSONUtil.toBean(shopJson, Shop.class);return Result.ok(shop);}// 判斷命中的是否是空值if(shopJson != null) {// 返回錯誤信息,解決緩存穿透問題return Result.fail("店鋪信息不存在!");}// 4.不存在,根據id查詢數據庫Shop shop = getById(id);if (shop == null) {// 5.數據庫不存在,將空字串寫入Redis,設置過期時間,解決緩存穿透問題stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回錯誤信息,解決緩存穿透問題return Result.fail("店鋪不存在!");}// 6.存在,寫入redisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);return Result.ok(shop);}
總結:緩存穿透的解決方案需結合業務場景選擇,通常需要多種手段協同(如布隆過濾器攔截非法 Key + 緩存空對象減少數據庫壓力)。同時需權衡內存、一致性和性能,避免過度設計。