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

// 1. 注入RedisTemplate(SpringBoot環(huán)境)
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 2. 互斥鎖核心方法(獲取鎖+查詢數(shù)據(jù)庫(kù)+更新緩存)
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); // 鎖過(guò)期時(shí)間3秒(根據(jù)數(shù)據(jù)庫(kù)查詢耗時(shí)調(diào)整)
if (isLock) {
try {
// 第三步:獲取鎖成功,查詢數(shù)據(jù)庫(kù)
value = queryDatabase(key); // 自定義方法,查詢數(shù)據(jù)庫(kù)數(shù)據(jù)
// 第四步:將數(shù)據(jù)庫(kù)數(shù)據(jù)寫入緩存,設(shè)置過(guò)期時(shí)間(避免再次擊穿)
redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);
return value;
} finally {
// 第五步:釋放鎖(必須在finally中,避免死鎖)
// 對(duì)比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ù)庫(kù)查詢方法
private Object queryDatabase(String key) {
// 實(shí)際業(yè)務(wù)中替換為真實(shí)數(shù)據(jù)庫(kù)查詢邏輯(如MyBatis查詢)
return "數(shù)據(jù)庫(kù)查詢到的" + key + "對(duì)應(yīng)數(shù)據(jù)";
}方案二:邏輯過(guò)期
我們不再設(shè)置key的物理過(guò)期時(shí)間,而是在緩存數(shù)據(jù)中嵌入一個(gè)邏輯過(guò)期時(shí)間字段。
當(dāng)大量請(qǐng)求過(guò)來(lái)后走緩存判斷查詢字段是否已經(jīng)過(guò)期
- 未過(guò)期:直接返回緩存數(shù)據(jù)
- 已過(guò)期:獲取互斥鎖, 此時(shí)開啟一個(gè)新的線程去查詢數(shù)據(jù)庫(kù)重建緩存,并且將其邏輯時(shí)間設(shè)置為 當(dāng)前時(shí)間+xxx分鐘,釋放鎖的任務(wù)交給該新線程,開啟新的線程后,直接返回過(guò)期的數(shù)據(jù)。在重建緩存過(guò)程中,其他線程進(jìn)來(lái)查詢緩存判斷邏輯時(shí)間已經(jīng)過(guò)期,且獲取鎖失敗,直接給它返回剛剛緩存中查詢出來(lái)的過(guò)期數(shù)據(jù)即可。
- 特點(diǎn):不能保證數(shù)據(jù)絕對(duì)一致,但是高可用、性能優(yōu)秀

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

