最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Redis緩存擊穿的用法及說(shuō)明

 更新時(shí)間:2026年04月26日 10:24:25   作者:風(fēng)吹迎面入袖涼  
文章介紹了緩存擊穿的概念、核心原因及其對(duì)系統(tǒng)的危害,并提出了兩種解決方案:互斥鎖方案和邏輯過(guò)期方案,互斥鎖方案能保證數(shù)據(jù)一致性但性能較低,邏輯過(guò)期方案則能提供高可用性和性能

一. 什么是緩存擊穿

緩存擊穿,指的是某一個(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&lt;Object&gt; 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&lt;Object&gt; 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)文章

最新評(píng)論

越西县| 浦城县| 城步| 抚远县| 揭阳市| 永靖县| 凤山县| 高邑县| 石泉县| 乌苏市| 洞口县| 岑溪市| 聊城市| 平邑县| 宁阳县| 洪泽县| 樟树市| 河源市| 长子县| 陈巴尔虎旗| 龙陵县| 宁陕县| 镇赉县| 云和县| 海晏县| 南雄市| 东城区| 长子县| 鄯善县| 宁安市| 岳阳市| 商水县| 屯昌县| 兰坪| 台中市| 原阳县| 通渭县| 佛山市| 泰来县| 茶陵县| 阿拉善盟|