Redis緩存擊穿的用法及說明
一. 什么是緩存擊穿
緩存擊穿,指的是某一個熱點key(被高頻訪問的key),在緩存中過期失效的瞬間,大量的請求直接穿透緩存,涌向數(shù)據(jù)庫,導(dǎo)致數(shù)據(jù)庫瞬時壓力驟增,甚至被壓垮的現(xiàn)象。
Tip:當(dāng)請求打過來時,未在緩存中命中,去數(shù)據(jù)庫查詢數(shù)據(jù)就可能涉及到了很多表和數(shù)據(jù)庫的查詢匯總。這種情況就會出現(xiàn)查詢數(shù)據(jù)時間很長,在這期間大量請求涌入數(shù)據(jù)庫,它可能瞬間扛不住,就會出現(xiàn)響應(yīng)超時、報錯,甚至宕機
舉個例子:比如電商平臺的“爆款商品詳情頁”,這個商品的key在Redis中緩存了1小時,平時所有請求都走緩存,數(shù)據(jù)庫幾乎無壓力。但當(dāng)這個key過期的那一刻,剛好有1000個用戶同時訪問這個商品,此時緩存中沒有數(shù)據(jù),所有1000個請求都會直接打向數(shù)據(jù)庫,數(shù)據(jù)庫瞬間扛不住,就會出現(xiàn)響應(yīng)超時、報錯,甚至宕機。
這里要注意區(qū)分兩個易混淆概念:
緩存穿透:是請求的key本身就不存在于緩存和數(shù)據(jù)庫中,請求一直穿透到數(shù)據(jù)庫。
緩存擊穿:是請求的key存在于數(shù)據(jù)庫中,但緩存剛好過期,瞬時請求穿透到數(shù)據(jù)庫。
二. 緩存擊穿的核心原因
1. 存在高頻訪問的熱點key
如果key不是熱點,即使緩存過期,也只有少量請求穿透到數(shù)據(jù)庫,不會造成太大影響。只有當(dāng)key被高頻訪問,才會在緩存失效瞬間對數(shù)據(jù)庫造成巨大壓力。
2. 緩存key過期失效
Redis的key都有過期時間,目的是為了釋放內(nèi)存,避免無效數(shù)據(jù)占用空間。但如果熱點key的過期時間設(shè)置不合理,或者剛好在請求高峰時過期,就會觸發(fā)擊穿。
比如:把爆款商品的緩存時間設(shè)置為1小時,而剛好在晚上8點(用戶訪問高峰)過期,就極易引發(fā)擊穿。
3. 緩存與數(shù)據(jù)庫之間無兜底機制
如果緩存失效后,沒有任何限流、降級、重試的機制,所有請求會毫無阻攔地沖向數(shù)據(jù)庫,而數(shù)據(jù)庫的并發(fā)處理能力遠低于Redis,很容易被壓垮。
三. 緩存擊穿的危害
數(shù)據(jù)庫壓力驟增:瞬時大量請求穿透到數(shù)據(jù)庫,導(dǎo)致數(shù)據(jù)庫CPU、內(nèi)存、IO占用率飆升,響應(yīng)時間大幅延長。
系統(tǒng)響應(yīng)超時:數(shù)據(jù)庫處理不過來請求,會導(dǎo)致接口響應(yīng)超時,前端出現(xiàn)加載失敗、白屏等問題。
數(shù)據(jù)庫宕機:如果請求量過大,超過數(shù)據(jù)庫的承載極限,會導(dǎo)致數(shù)據(jù)庫宕機,進而引發(fā)整個系統(tǒng)服務(wù)不可用。
連鎖反應(yīng):數(shù)據(jù)庫宕機后,即使緩存恢復(fù),后續(xù)請求依然無法正常處理,可能導(dǎo)致服務(wù)雪崩,依賴該數(shù)據(jù)庫的其他服務(wù)也跟著報錯。
四. 緩存擊穿的解決方案
方案一:互斥鎖
當(dāng)大量請求過來后走緩存
- 命中:直接返回緩存數(shù)據(jù)
- 未命中:讓一個請求獲取到鎖去查詢數(shù)據(jù)庫重建緩存數(shù)據(jù),其余未命中且未獲取到鎖的請求則休眠一段時間后,再次查詢緩存。
- 特點:保證了很強的一致性,但是性能很差(有一段時間內(nèi)其余未獲取鎖請求都在空閑等待)

// 1. 注入RedisTemplate(SpringBoot環(huán)境)
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 2. 互斥鎖核心方法(獲取鎖+查詢數(shù)據(jù)庫+更新緩存)
public Object getValueByMutexLock(String key) {
// 第一步:查詢緩存
Object value = redisTemplate.opsForValue().get(key);
if (value != null) {
return value; // 緩存存在,直接返回
}
// 第二步:緩存不存在,嘗試獲取分布式鎖
String lockKey = "lock:" + key; // 鎖key,與業(yè)務(wù)key綁定,避免鎖沖突
String lockValue = UUID.randomUUID().toString(); // 唯一值,用于釋放鎖
boolean isLock = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, 3, TimeUnit.SECONDS); // 鎖過期時間3秒(根據(jù)數(shù)據(jù)庫查詢耗時調(diào)整)
if (isLock) {
try {
// 第三步:獲取鎖成功,查詢數(shù)據(jù)庫
value = queryDatabase(key); // 自定義方法,查詢數(shù)據(jù)庫數(shù)據(jù)
// 第四步:將數(shù)據(jù)庫數(shù)據(jù)寫入緩存,設(shè)置過期時間(避免再次擊穿)
redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);
return value;
} finally {
// 第五步:釋放鎖(必須在finally中,避免死鎖)
// 對比value確保是自己的鎖,避免誤釋放他人的鎖
if (lockValue.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
}
}
} else {
// 第六步:獲取鎖失敗,重試(間隔100ms,避免頻繁重試)
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return getValueByMutexLock(key); // 遞歸重試,也可使用循環(huán)
}
}
// 模擬數(shù)據(jù)庫查詢方法
private Object queryDatabase(String key) {
// 實際業(yè)務(wù)中替換為真實數(shù)據(jù)庫查詢邏輯(如MyBatis查詢)
return "數(shù)據(jù)庫查詢到的" + key + "對應(yīng)數(shù)據(jù)";
}方案二:邏輯過期
我們不再設(shè)置key的物理過期時間,而是在緩存數(shù)據(jù)中嵌入一個邏輯過期時間字段。
當(dāng)大量請求過來后走緩存判斷查詢字段是否已經(jīng)過期
- 未過期:直接返回緩存數(shù)據(jù)
- 已過期:獲取互斥鎖, 此時開啟一個新的線程去查詢數(shù)據(jù)庫重建緩存,并且將其邏輯時間設(shè)置為 當(dāng)前時間+xxx分鐘,釋放鎖的任務(wù)交給該新線程,開啟新的線程后,直接返回過期的數(shù)據(jù)。在重建緩存過程中,其他線程進來查詢緩存判斷邏輯時間已經(jīng)過期,且獲取鎖失敗,直接給它返回剛剛緩存中查詢出來的過期數(shù)據(jù)即可。
- 特點:不能保證數(shù)據(jù)絕對一致,但是高可用、性能優(yōu)秀

// 1. 定義緩存數(shù)據(jù)封裝類(封裝業(yè)務(wù)數(shù)據(jù)+邏輯過期時間)
@Data
public class CacheData<T> {
// 業(yè)務(wù)數(shù)據(jù)
private T data;
// 邏輯過期時間(時間戳,單位:毫秒)
private Long expireTime;
}
// 2. 注入依賴(SpringBoot環(huán)境)
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 異步線程池(用于邏輯過期后異步更新緩存)
@Autowired
private ThreadPoolTaskExecutor asyncTaskExecutor;
// 3. 邏輯過期核心方法
public Object getValueByLogicalExpire(String key) {
// 第一步:查詢Redis緩存(獲取封裝后的CacheData對象)
CacheData<Object> cacheData = (CacheData<Object>) redisTemplate.opsForValue().get(key);
if (cacheData == null) {
// 緩存不存在(首次請求/緩存被手動刪除),此處可返回兜底數(shù)據(jù)或查詢數(shù)據(jù)庫
return queryDatabase(key);
}
// 第二步:判斷邏輯過期時間是否已到
Long currentTime = System.currentTimeMillis();
if (currentTime < cacheData.getExpireTime()) {
// 邏輯未過期,直接返回業(yè)務(wù)數(shù)據(jù)
return cacheData.getData();
}
// 第三步:邏輯已過期,返回舊數(shù)據(jù),同時異步更新緩存
asyncTaskExecutor.execute(() -> {
try {
// 異步查詢數(shù)據(jù)庫最新數(shù)據(jù)
Object newData = queryDatabase(key);
// 重新封裝CacheData,設(shè)置新的邏輯過期時間(如30分鐘后)
CacheData<Object> newCacheData = new CacheData<>();
newCacheData.setData(newData);
newCacheData.setExpireTime(System.currentTimeMillis() + 30 * 60 * 1000);
// 更新Redis緩存(無物理過期時間)
redisTemplate.opsForValue().set(key, newCacheData);
} catch (Exception e) {
// 異常處理(如日志記錄),避免異步任務(wù)失敗導(dǎo)致緩存無法更新
log.error("邏輯過期緩存更新失敗,key:{}", key, e);
}
});
// 直接返回舊數(shù)據(jù),不阻塞當(dāng)前請求
return cacheData.getData();
}
// 模擬數(shù)據(jù)庫查詢方法(與互斥鎖方案一致)
private Object queryDatabase(String key) {
return "數(shù)據(jù)庫查詢到的" + key + "對應(yīng)數(shù)據(jù)";
}
// 4. 初始化緩存(緩存預(yù)熱,存入帶邏輯過期時間的數(shù)據(jù))
public void initCache(String key) {
Object data = queryDatabase(key);
CacheData<Object> cacheData = new CacheData<>();
cacheData.setData(data);
// 設(shè)置邏輯過期時間(30分鐘)
cacheData.setExpireTime(System.currentTimeMillis() + 30 * 60 * 1000);
// 存入Redis,不設(shè)置物理過期時間
redisTemplate.opsForValue().set(key, cacheData);
}總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Python利用redis限制用戶重復(fù)刷新帶來的數(shù)據(jù)問題
在網(wǎng)站開發(fā)中,我們經(jīng)常會遇到需要控制用戶重復(fù)刷新頁面的情況,本文就來介紹了Python利用redis限制用戶重復(fù)刷新帶來的數(shù)據(jù)問題,感興趣的可以了解一下2024-03-03
Redis官方可視化工具RedisInsight的安裝使用詳細教程(功能強大)
RedisInsight是Redis官方出品的可視化管理工具,可用于設(shè)計、開發(fā)、優(yōu)化你的Redis應(yīng)用。支持深色和淺色兩種主題,界面非常炫酷,接下來通過本文給大家介紹Redis官方可視化工具RedisInsight的安裝使用過程,需要的朋友可以參考下2022-04-04
監(jiān)聽Redis實時數(shù)據(jù)變化實現(xiàn)過程
本文介紹了通過定時遍歷Redis并對比緩存Map,實現(xiàn)監(jiān)聽指定前綴鍵的value變化的方法,并簡要提及了可用Keyspace通知等更高效的手段2025-10-10

