Redis之緩存擊穿
文章目錄
- Redis之緩存擊穿
- 一、什么是緩存擊穿
- 二、緩存擊穿常見解決方案
- 1. 互斥鎖(Mutex Lock)
- 2. 永不過期 + 后臺刷新
- 3. 邏輯過期(異步更新)
- 三、案例
- 1.基于互斥鎖解決緩存擊穿
- 2.基于邏輯過期解決緩存擊穿
- 四、注意事項
- 1.? 鎖的選擇
- 2. 遞歸重試風險
- 3. 鎖超時時間
- ?4. 緩存過期時間隨機化
一、什么是緩存擊穿
緩存擊穿(Cache Breakdown)是指某個熱點 Key 在緩存中過期后,大量并發請求同時繞過緩存直接訪問數據庫,導致數據庫壓力驟增的現象。
通常發生在以下場景:
- 某個 Key 是高頻訪問的「熱點數據」。
- Key 的緩存過期時間到期,此時大并發請求同時到達。
- 緩存失效瞬間,所有請求都去查詢數據庫并重建緩存。
二、緩存擊穿常見解決方案
1. 互斥鎖(Mutex Lock)
- 原理:當緩存失效時,只允許一個線程去加載數據,其他線程等待緩存更新完成后再讀取緩存。
- 優點: 確保數據強一致性
- 缺點:線程需要等待,可能成為性能瓶頸(鎖競爭)
- 流程圖:
- 偽代碼:
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;
}
2. 永不過期 + 后臺刷新
- 原理:為緩存設置永不過期時間,同時通過后臺線程主動更新緩存。
- 優點:無阻塞,適合對一致性要求低場景
- 缺點:數據可能短暫陳舊
- 流程圖:
- 偽代碼:
// 初始化時設置緩存永不過期
cache.set("hot_key", data)// 后臺線程定期更新
public void backgroundRefresh() {while(true) {Thread.sleep(5 * 60 * 1000) // 每5分鐘更新一次newData = db.query("hot_key")cache.set("hot_key", newData)}
}
3. 邏輯過期(異步更新)
- 原理:在緩存中存儲數據的邏輯過期時間,即使緩存未物理過期,若邏輯過期則異步更新。
- 優點:無阻塞,兼容性強
- 缺點:不保證一致性,實現復雜度較高
- 時序圖:
- 偽代碼:
緩存條目類
@Data
public class CacheEntry {private final String data;private final long expireTime;
}
// 獲取當前時間戳(毫秒)private static long now() {return System.currentTimeMillis();}public static String getData(String key) {CacheEntry entry = cache.get(key);// 緩存未命中if (entry == null) {String data = Database.query(key);long expireTime = now() + 300_000; // 5分鐘過期(300秒 * 1000)cache.put(key, new CacheEntry(data, expireTime));return data;}// 檢查邏輯過期if (entry.getExpireTime() < now()) {// 啟動異步更新線程new Thread(() -> asyncUpdate(key)).start();}// 返回過期數據return entry.getData();}private static void asyncUpdate(String key) {String newData = Database.query(key);long newExpireTime = now() + 300_000;cache.put(key, new CacheEntry(newData, newExpireTime));}
三、案例
1.基于互斥鎖解決緩存擊穿
public Shop queryWithMutex(Long id) {// 1.從redis查詢商鋪緩存String key = CACHE_SHOP_KEY + id;String shopJson = stringRedisTemplate.opsForValue().get(key);// 2.判斷是否存在if (StrUtil.isNotBlank(shopJson)) {// 3.存在,直接返回return JSONUtil.toBean(shopJson, Shop.class);}// 判斷命中的是否是空值if(shopJson != null) {// 返回錯誤信息,解決緩存穿透問題return null;}// 4.實現緩存重建// 4.1 獲取互斥鎖String lockKey = LOCK_SHOP_KEY + id;Shop shop;try {boolean isLock = tryLock(lockKey);// 4.2 判斷是否獲取成功if (!isLock) {// 4.3 失敗,則休眠并重試Thread.sleep(50);return queryWithMutex(id);}// 4.4 成功,根據id查詢數據庫,返回數據shop = getById(id);if (shop == null) {// 5.數據庫不存在,將空字串寫入Redis,設置過期時間,解決緩存穿透問題stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回錯誤信息,解決緩存穿透問題return null;}// 6.存在,寫入redisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {// 7.釋放鎖unLock(lockKey);}return shop;}
2.基于邏輯過期解決緩存擊穿
public Shop queryWithLogicalExpire(Long id) {// 1.從redis查詢商鋪緩存String key = CACHE_SHOP_KEY + id;String shopJson = stringRedisTemplate.opsForValue().get(key);// 2.判斷是否存在if (StrUtil.isBlank(shopJson)) {// 3.不存在,直接返回nullreturn null;}// 4.命中,需要先把json反序列化為對象RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);JSONObject data = (JSONObject) redisData.getData();Shop shop = JSONUtil.toBean(data, Shop.class);LocalDateTime expireTime = redisData.getExpireTime();// 5.判斷是否過期if (expireTime.isAfter(LocalDateTime.now())) {// 5.1 未過期,直接返回數據return shop;}// 5.2 過期,需要緩存重建// 6. 緩存重建String lockKey = LOCK_SHOP_KEY + id;// 6.1 獲取互斥鎖boolean isLock = tryLock(lockKey);// 6.2 判斷是否獲取鎖成功if (isLock) {// 6.3 成功,開啟獨立線程,實現緩存重建CACHE_REBUILD_EXECUTOR.submit(() -> {try {this.saveShop2Redis(id, 20L);} catch (Exception e) {throw new RuntimeException(e);} finally {// 6.5 釋放鎖unLock(lockKey);}});}// 6.4 返回過期的商鋪信息return shop;}
四、注意事項
1.? 鎖的選擇
- 單機環境用
ReentrantLock
或synchronized
。 - 分布式環境需用 Redis 分布式鎖(如 Redisson 的
RLock
)。
2. 遞歸重試風險
- 示例中遞歸調用,可能導致棧溢出,實際生產環境應改用循環重試。
3. 鎖超時時間
- 分布式鎖需設置合理超時時間(如 300ms),防止死鎖。
?4. 緩存過期時間隨機化
- 可對緩存 TTL 添加隨機值(如
300 + rand.nextInt(100)
),避免緩存雪崩。