天天看點

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

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

一、前言

在我們日常的開發中,無不都是使用資料庫來進行資料的存儲,由于一般的系統任務中通常不會存在高并發的情況,是以這樣看起來并沒有什麼問題,可是一旦涉及大資料量的需求,比如一些商品搶購的情景,或者是首頁通路量瞬間較大的時候,單一使用資料庫來儲存資料的系統會因為面向磁盤,磁盤讀/寫速度比較慢的問題而存在嚴重的性能弊端,一瞬間成千上萬的請求到來,需要系統在極短的時間内完成成千上萬次的讀/寫操作,這個時候往往不是資料庫能夠承受的,極其容易造成資料庫系統癱瘓,最終導緻服務當機的嚴重生産問題。

為了克服上述的問題,項目通常會引入NoSQL技術,這是一種基于記憶體的資料庫,并且提供一定的持久化功能。

redis技術就是NoSQL技術中的一種,但是引入redis又有可能出現緩存穿透,緩存擊穿,緩存雪崩等問題。本文就對這三種問題進行較深入剖析。

二、初認識

緩存穿透:key對應的資料在資料源并不存在,每次針對此key的請求從緩存擷取不到,請求都會到資料源,進而可能壓垮資料源。比如用一個不存在的使用者id擷取使用者資訊,不論緩存還是資料庫都沒有,若黑客利用此漏洞進行攻擊可能壓垮資料庫。

緩存擊穿:key對應的資料存在,但在redis中過期,此時若有大量并發請求過來,這些請求發現緩存過期一般都會從後端DB加載資料并回設到緩存,這個時候大并發的請求可能會瞬間把後端DB壓垮。

緩存雪崩:當緩存伺服器重新開機或者大量緩存集中在某一個時間段失效,這樣在失效的時候,也會給後端系統(比如DB)帶來很大壓力。

三、緩存穿透解決方案

一個一定不存在緩存及查詢不到的資料,由于緩存是不命中時被動寫的,并且出于容錯考慮,如果從存儲層查不到資料則不寫入緩存,這将導緻這個不存在的資料每次請求都要到存儲層去查詢,失去了緩存的意義。

有很多種方法可以有效地解決緩存穿透問題,最常見的則是采用布隆過濾器,将所有可能存在的資料哈希到一個足夠大的bitmap中,一個一定不存在的資料會被 這個bitmap攔截掉,進而避免了對底層存儲系統的查詢壓力。另外也有一個更為簡單粗暴的方法(我們采用的就是這種),如果一個查詢傳回的資料為空(不管是資料不存在,還是系統故障),我們仍然把這個空結果進行緩存,但它的過期時間會很短,最長不超過五分鐘。

粗暴方式僞代碼:

//僞代碼

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 {
    //資料庫查詢不到,為空
    cacheValue = GetProductListFromDB();
    if (cacheValue == null) {
        //如果發現為空,設定個預設值,也緩存起來
        cacheValue = string.Empty;
    }
    CacheHelper.Add(cacheKey, cacheValue, cacheTime);
    return cacheValue;
}           

}

四、緩存擊穿解決方案

key可能會在某些時間點被超高并發地通路,是一種非常“熱點”的資料。這個時候,需要考慮一個問題:緩存被“擊穿”的問題。

使用互斥鎖(mutex key)

業界比較常用的做法,是使用mutex。簡單地來說,就是在緩存失效的時候(判斷拿出來的值為空),不是立即去load db,而是先使用緩存工具的某些帶成功操作傳回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一個mutex key,當操作傳回成功時,再進行load db的操作并回設緩存;否則,就重試整個get緩存的方法。

SETNX,是「SET if Not eXists」的縮寫,也就是隻有不存在的時候才設定,可以利用它來實作鎖的效果。

public String get(key) {

String value = redis.get(key);
  if (value == null) { //代表緩存值過期
      //設定3min的逾時,防止del操作失敗的時候,下次緩存過期一直不能load db
  if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {  //代表設定成功
           value = db.get(key);
                  redis.set(key, value, expire_secs);
                  redis.del(key_mutex);
          } else {  //這個時候代表同時候的其他線程已經load db并回設到緩存了,這時候重試擷取緩存值即可
                  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();  
}             

其它方案:待各位補充。

五、緩存雪崩解決方案

與緩存擊穿的差別在于這裡針對很多key緩存,前者則是某一個key。

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

redis1.md

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

redis2.md

緩存失效時的雪崩效應對底層系統的沖擊非常可怕!大多數系統設計者考慮用加鎖或者隊列的方式保證來保證不會有大量的線程對資料庫一次性進行讀寫,進而避免失效時大量的并發請求落到底層存儲系統上。還有一個簡單方案就時講緩存失效時間分散開,比如我們可以在原有的失效時間基礎上增加一個随機值,比如1-5分鐘随機,這樣每一個緩存的過期時間的重複率就會降低,就很難引發集體失效的事件。

加鎖排隊,僞代碼如下:

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查詢資料
            cacheValue = GetProductListFromDB(); 
            CacheHelper.Add(cacheKey, cacheValue, cacheTime);
        }
    }
    return cacheValue;
}           

加鎖排隊隻是為了減輕資料庫的壓力,并沒有提高系統吞吐量。假設在高并發下,緩存重建期間key是鎖着的,這是過來1000個請求999個都在阻塞的。同樣會導緻使用者等待逾時,這是個治标不治本的方法!

注意:加鎖排隊的解決方式分布式環境的并發問題,有可能還要解決分布式鎖的問題;線程還會被阻塞,使用者體驗很差!是以,在真正的高并發場景下很少使用!

随機值僞代碼:

int cacheTime = 30;
String cacheKey = "product_list";
//緩存标記
String cacheSign = cacheKey + "_sign";

String sign = CacheHelper.Get(cacheSign);
//擷取緩存值
String cacheValue = CacheHelper.Get(cacheKey);
if (sign != null) {
    return cacheValue; //未過期,直接傳回
} else {
    CacheHelper.Add(cacheSign, "1", cacheTime);
    ThreadPool.QueueUserWorkItem((arg) -> {
  //這裡一般是 sql查詢資料
        cacheValue = GetProductListFromDB(); 
      //日期設緩存時間的2倍,用于髒讀
      CacheHelper.Add(cacheKey, cacheValue, cacheTime * 2);                 
    });
    return cacheValue;
}           

}

解釋說明:

緩存标記:記錄緩存資料是否過期,如果過期會觸發通知另外的線程在背景去更新實際key的緩存;

緩存資料:它的過期時間比緩存标記的時間延長1倍,例:标記緩存時間30分鐘,資料緩存設定為60分鐘。這樣,當緩存标記key過期後,實際緩存還能把舊資料傳回給調用端,直到另外的線程在背景更新完成後,才會傳回新緩存。

關于緩存崩潰的解決方法,這裡提出了三種方案:使用鎖或隊列、設定過期标志更新緩存、為key設定不同的緩存失效時間,還有一種被稱為“二級緩存”的解決方法。

六、小結

針對業務系統,永遠都是具體情況具體分析,沒有最好,隻有最合适。

于緩存其它問題,緩存滿了和資料丢失等問題,大夥可自行學習。最後也提一下三個詞LRU、RDB、AOF,通常我們采用LRU政策處理溢出,Redis的RDB和AOF持久化政策來保證一定情況下的資料安全。

參考相關連結:

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

原文位址

https://www.cnblogs.com/midoujava/p/11277096.html