REDIS 緩存穿透,緩存擊穿,緩存雪崩原因 + 解決方案

一、前言

在我們?nèi)粘5拈_發(fā)中,無(wú)不都是使用數(shù)據(jù)庫(kù)來(lái)進(jìn)行數(shù)據(jù)的存儲(chǔ),由于一般的系統(tǒng)任務(wù)中通常不會(huì)存在高并發(fā)的情況,所以這樣看起來(lái)并沒有什么問(wèn)題,可是一旦涉及大數(shù)據(jù)量的需求,比如一些商品搶購(gòu)的情景,或者是主頁(yè)訪問(wèn)量瞬間較大的時(shí)候,單一使用數(shù)據(jù)庫(kù)來(lái)保存數(shù)據(jù)的系統(tǒng)會(huì)因?yàn)槊嫦虼疟P,磁盤讀/寫速度比較慢的問(wèn)題而存在嚴(yán)重的性能弊端,一瞬間成千上萬(wàn)的請(qǐng)求到來(lái),需要系統(tǒng)在極短的時(shí)間內(nèi)完成成千上萬(wàn)次的讀/寫操作,這個(gè)時(shí)候往往不是數(shù)據(jù)庫(kù)能夠承受的,極其容易造成數(shù)據(jù)庫(kù)系統(tǒng)癱瘓,最終導(dǎo)致服務(wù)宕機(jī)的嚴(yán)重生產(chǎn)問(wèn)題。

為了克服上述的問(wèn)題,項(xiàng)目通常會(huì)引入NoSQL技術(shù),這是一種基于內(nèi)存的數(shù)據(jù)庫(kù),并且提供一定的持久化功能。

redis技術(shù)就是NoSQL技術(shù)中的一種,但是引入redis又有可能出現(xiàn)緩存穿透,緩存擊穿,緩存雪崩等問(wèn)題。本文就對(duì)這三種問(wèn)題進(jìn)行較深入剖析。

前臺(tái)請(qǐng)求,后臺(tái)先從緩存中取數(shù)據(jù),取到直接返回結(jié)果,取不到時(shí)從數(shù)據(jù)庫(kù)中取,數(shù)據(jù)庫(kù)取到更新緩存,并返回結(jié)果,數(shù)據(jù)庫(kù)也沒取到,那直接返回空結(jié)果。

image

二、初認(rèn)識(shí)

  • 緩存穿透:key對(duì)應(yīng)的數(shù)據(jù)在數(shù)據(jù)源并不存在,每次針對(duì)此key的請(qǐng)求從緩存獲取不到,請(qǐng)求都會(huì)到數(shù)據(jù)源,從而可能壓垮數(shù)據(jù)源。比如用一個(gè)不存在的用戶id獲取用戶信息,不論緩存還是數(shù)據(jù)庫(kù)都沒有,若黑客利用此漏洞進(jìn)行攻擊可能壓垮數(shù)據(jù)庫(kù)。
  • 緩存擊穿:key對(duì)應(yīng)的數(shù)據(jù)存在,但在redis中過(guò)期,此時(shí)若有大量并發(fā)請(qǐng)求過(guò)來(lái),這些請(qǐng)求發(fā)現(xiàn)緩存過(guò)期一般都會(huì)從后端DB加載數(shù)據(jù)并回設(shè)到緩存,這個(gè)時(shí)候大并發(fā)的請(qǐng)求可能會(huì)瞬間把后端DB壓垮。
  • 緩存雪崩:當(dāng)緩存服務(wù)器重啟或者大量緩存集中在某一個(gè)時(shí)間段失效,這樣在失效的時(shí)候,也會(huì)給后端系統(tǒng)(比如DB)帶來(lái)很大壓力。

三、緩存穿透解決方案

一個(gè)一定不存在緩存及查詢不到的數(shù)據(jù),由于緩存是不命中時(shí)被動(dòng)寫的,并且出于容錯(cuò)考慮,如果從存儲(chǔ)層查不到數(shù)據(jù)則不寫入緩存,這將導(dǎo)致這個(gè)不存在的數(shù)據(jù)每次請(qǐng)求都要到存儲(chǔ)層去查詢,失去了緩存的意義。

有很多種方法可以有效地解決緩存穿透問(wèn)題最常見的則是采用布隆過(guò)濾器,將所有可能存在的數(shù)據(jù)哈希到一個(gè)足夠大的bitmap中,一個(gè)一定不存在的數(shù)據(jù)會(huì)被 這個(gè)bitmap攔截掉,從而避免了對(duì)底層存儲(chǔ)系統(tǒng)的查詢壓力。另外也有一個(gè)更為簡(jiǎn)單粗暴的方法(我們采用的就是這種),如果一個(gè)查詢返回的數(shù)據(jù)為空(不管是數(shù)據(jù)不存在,還是系統(tǒng)故障),我們?nèi)匀话堰@個(gè)空結(jié)果進(jìn)行緩存,但它的過(guò)期時(shí)間會(huì)很短,最長(zhǎng)不超過(guò)五分鐘。

粗暴方式偽代碼:

//偽代碼
public object GetProductListNew() {
    int cacheTime = 30;
    String cacheKey = "product_list";

    String cacheValue = CacheHelper.Get(cacheKey);
    if (cacheValue != null) {
        return cacheValue;
    }

    cacheValue = CacheHelper.Get(cacheKey);
    if (cacheValue != null) {
        return cacheValue;
    } else {
        //數(shù)據(jù)庫(kù)查詢不到,為空
        cacheValue = GetProductListFromDB();
        if (cacheValue == null) {
            //如果發(fā)現(xiàn)為空,設(shè)置個(gè)默認(rèn)值,也緩存起來(lái)
            cacheValue = string.Empty;
        }
        CacheHelper.Add(cacheKey, cacheValue, cacheTime);
        return cacheValue;
    }
}

四、緩存擊穿解決方案

key可能會(huì)在某些時(shí)間點(diǎn)被超高并發(fā)地訪問(wèn),是一種非?!盁狳c(diǎn)”的數(shù)據(jù)。這個(gè)時(shí)候,需要考慮一個(gè)問(wèn)題:緩存被“擊穿”的問(wèn)題。

使用互斥鎖(mutex key)

業(yè)界比較常用的做法,是使用mutex。簡(jiǎn)單地來(lái)說(shuō),就是在緩存失效的時(shí)候(判斷拿出來(lái)的值為空),不是立即去load db,而是先使用緩存工具的某些帶成功操作返回值的操作(比如Redis的SETNX或者M(jìn)emcache的ADD)去set一個(gè)mutex key,當(dāng)操作返回成功時(shí),再進(jìn)行l(wèi)oad db的操作并回設(shè)緩存;否則,就重試整個(gè)get緩存的方法。

SETNX,是「SET if Not eXists」的縮寫,也就是只有不存在的時(shí)候才設(shè)置,可以利用它來(lái)實(shí)現(xiàn)鎖的效果。

public String get(key) {
      String value = redis.get(key);
      if (value == null) { //代表緩存值過(guò)期
          //設(shè)置3min的超時(shí),防止del操作失敗的時(shí)候,下次緩存過(guò)期一直不能load db
          if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {  //代表設(shè)置成功
             value = db.get(key);
             redis.set(key, value, expire_secs);
             redis.del(key_mutex);
          } else {  //這個(gè)時(shí)候代表同時(shí)候的其他線程已經(jīng)load db并回設(shè)到緩存了,這時(shí)候重試獲取緩存值即可
             sleep(50);
             get(key);  //重試
          }
      } else {
          return value;      
      }
}

memcache代碼:

if (memcache.get(key) == null) {  
    // 3 min timeout to avoid mutex holder crash  
    if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {  
        value = db.get(key);  
        memcache.set(key, value);  
        memcache.delete(key_mutex);  
    } else {  
        sleep(50);  
        retry();  
    }  
}

其它方案:待各位補(bǔ)充。

五、緩存雪崩解決方案

與緩存擊穿的區(qū)別在于這里針對(duì)很多key緩存,前者則是某一個(gè)key。

緩存正常從Redis中獲取,示意圖如下:


redis1.md

緩存失效瞬間示意圖如下:


redis2.md

緩存失效時(shí)的雪崩效應(yīng)對(duì)底層系統(tǒng)的沖擊非??膳拢〈蠖鄶?shù)系統(tǒng)設(shè)計(jì)者考慮用加鎖或者隊(duì)列的方式保證來(lái)保證不會(huì)有大量的線程對(duì)數(shù)據(jù)庫(kù)一次性進(jìn)行讀寫,從而避免失效時(shí)大量的并發(fā)請(qǐng)求落到底層存儲(chǔ)系統(tǒng)上。還有一個(gè)簡(jiǎn)單方案就時(shí)講緩存失效時(shí)間分散開,比如我們可以在原有的失效時(shí)間基礎(chǔ)上增加一個(gè)隨機(jī)值,比如1-5分鐘隨機(jī),這樣每一個(gè)緩存的過(guò)期時(shí)間的重復(fù)率就會(huì)降低,就很難引發(fā)集體失效的事件。

加鎖排隊(duì),偽代碼如下:

//偽代碼
public object GetProductListNew() {
    int cacheTime = 30;
    String cacheKey = "product_list";
    String lockKey = cacheKey;

    String cacheValue = CacheHelper.get(cacheKey);
    if (cacheValue != null) {
        return cacheValue;
    } else {
        synchronized(lockKey) {
            cacheValue = CacheHelper.get(cacheKey);
            if (cacheValue != null) {
                return cacheValue;
            } else {
                //這里一般是sql查詢數(shù)據(jù)
                cacheValue = GetProductListFromDB(); 
                CacheHelper.Add(cacheKey, cacheValue, cacheTime);
            }
        }
        return cacheValue;
    }
}

加鎖排隊(duì)只是為了減輕數(shù)據(jù)庫(kù)的壓力,并沒有提高系統(tǒng)吞吐量。假設(shè)在高并發(fā)下,緩存重建期間key是鎖著的,這是過(guò)來(lái)1000個(gè)請(qǐng)求999個(gè)都在阻塞的。同樣會(huì)導(dǎo)致用戶等待超時(shí),這是個(gè)治標(biāo)不治本的方法!

注意:加鎖排隊(duì)的解決方式分布式環(huán)境的并發(fā)問(wèn)題,有可能還要解決分布式鎖的問(wèn)題;線程還會(huì)被阻塞,用戶體驗(yàn)很差!因此,在真正的高并發(fā)場(chǎng)景下很少使用!

隨機(jī)值偽代碼:

//偽代碼
public object GetProductListNew() {
    int cacheTime = 30;
    String cacheKey = "product_list";
    //緩存標(biāo)記
    String cacheSign = cacheKey + "_sign";

    String sign = CacheHelper.Get(cacheSign);
    //獲取緩存值
    String cacheValue = CacheHelper.Get(cacheKey);
    if (sign != null) {
        return cacheValue; //未過(guò)期,直接返回
    } else {
        CacheHelper.Add(cacheSign, "1", cacheTime);
        ThreadPool.QueueUserWorkItem((arg) -> {
           //這里一般是 sql查詢數(shù)據(jù)
           cacheValue = GetProductListFromDB(); 
           //日期設(shè)緩存時(shí)間的2倍,用于臟讀
           CacheHelper.Add(cacheKey, cacheValue, cacheTime * 2);                 
        });
        return cacheValue;
    }
} 

解釋說(shuō)明:

  • 緩存標(biāo)記:記錄緩存數(shù)據(jù)是否過(guò)期,如果過(guò)期會(huì)觸發(fā)通知另外的線程在后臺(tái)去更新實(shí)際key的緩存;
  • 緩存數(shù)據(jù):它的過(guò)期時(shí)間比緩存標(biāo)記的時(shí)間延長(zhǎng)1倍,例:標(biāo)記緩存時(shí)間30分鐘,數(shù)據(jù)緩存設(shè)置為60分鐘。這樣,當(dāng)緩存標(biāo)記key過(guò)期后,實(shí)際緩存還能把舊數(shù)據(jù)返回給調(diào)用端,直到另外的線程在后臺(tái)更新完成后,才會(huì)返回新緩存。

其它解決方案:

  • 緩存數(shù)據(jù)的過(guò)期時(shí)間設(shè)置隨機(jī),防止同一時(shí)間大量數(shù)據(jù)過(guò)期現(xiàn)象發(fā)生。
  • 如果緩存數(shù)據(jù)庫(kù)是分布式部署,將熱點(diǎn)數(shù)據(jù)均勻分布在不同的緩存數(shù)據(jù)庫(kù)中。
  • 設(shè)置熱點(diǎn)數(shù)據(jù)永遠(yuǎn)不過(guò)期。

六、小結(jié)

針對(duì)業(yè)務(wù)系統(tǒng),永遠(yuǎn)都是具體情況具體分析,沒有最好,只有最合適。

于緩存其它問(wèn)題,緩存滿了和數(shù)據(jù)丟失等問(wèn)題,大伙可自行學(xué)習(xí)。最后也提一下三個(gè)詞LRU、RDB、AOF,通常我們采用LRU策略處理溢出,Redis的RDB和AOF持久化策略來(lái)保證一定情況下的數(shù)據(jù)安全。

參考相關(guān)鏈接:

https://blog.csdn.net/zeb_perfect/article/details/54135506
https://blog.csdn.net/fanrenxiang/article/details/80542580
https://baijiahao.baidu.com/s?id=1619572269435584821&wfr=spider&for=pc
https://blog.csdn.net/xlgen157387/article/details/79530877

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容