天天看點

Redis緩存雪崩、緩存穿透、緩存擊穿解決方案詳解(上)1 緩存雪崩(Cache Avalanche)2 緩存穿透(Cache Penetration)

1 緩存雪崩(Cache Avalanche)

1.1 什麼是緩存雪崩?

由于

  • 應用設計層面,大量Key同時過期
  • 緩存服務當機

等原因,可能導緻緩存資料同一時刻大規模不可用,或者都更新。

集中過期,其實不是太緻命,最緻命的是緩存伺服器某個節點當機:

  • 自然形成的緩存雪崩,一定是在某個時間段集中建立緩存,那麼這時DB也可頂住壓力,無非就是對DB産生周期性壓力
  • 而緩存服務節點的當機,這時所有緩存 key 都沒了,請求全部打入 DB,對DB造成的壓力不可預知,很可能瞬間就把DB壓垮,需通過主從叢集哨兵等解決

像電商項目,一般采取将不同分類的商品,緩存不同周期。在同一分類中的商品,加上一個随機因子。盡可能分散緩存過期時間,而且,熱門類目的商品緩存時間長一些,冷門類目的商品緩存時間短一些,也能節省緩存服務的資源。

1.2 解決方案

  • 更新政策在時間上做到比較均勻
  • 使用的熱資料盡量分散到不同的機器上
  • 多台機器做主從複制或多副本,實作高可用

1.2.1 差異化緩存過期時間

不要讓大量Key同時過期。

在原有失效時間基礎上增加一個随機值,比如1~5分鐘的随機,這樣每個緩存的過期時間重複率就會降低,集體失效機率也會大大降低。

1.2.2 讓緩存不主動過期

初始化緩存資料的時候設定緩存永不過期,然後啟動一個背景線程30秒一次定時把所有資料更新到緩存,而且通過适當休眠,控制從DB更新資料的頻率,降低DB壓力。

兩種解決方案截然不同,若無法全量緩存所有資料,則隻能使用方案一。

即使使用了方案二,緩存永不過期,同樣需在查詢時,確定有回源的邏輯。因為我們無法確定緩存系統中的資料永不丢失。

不管哪個方案,在把資料從DB加入緩存時,都需判斷來自DB的資料是否合法,比如最基本的判空!不然在某個時間點,如果DBA把 DB原始資料歸檔了。

因為緩存中的資料一直在是以一開始沒什麼問題,但也許N年後,某天緩存資料突然過期了,就從DB查到空資料加入緩存!

2 緩存穿透(Cache Penetration)

2.1 什麼是緩存穿透?

高并發查詢不存在的key,導緻将壓力都直接透傳到DB。

  • 為何會多次透傳?

    因為緩存不存在該資料,一直為空。

注意讓緩存能夠區分 key 是不存在 or 存在但查詢得到一個空值。

例如:通路id=-1的資料。可能出現繞過Redis頻繁通路DB,稱為緩存穿透,多出現在查詢為null的情況不被緩存時。

2.2 解決方案

布隆過濾器 or RoaringBitmap

提供一個能迅速判斷請求是否有效的攔截機制。

比如利用布隆過濾器,維護一系列合法有效的 key。進而能迅速判斷出,請求所攜帶的 Key 是否合法有效:

  • 若不合法,則直接傳回,避免直接查詢DB。

緩存空值key

如果從DB查詢的對象為空,也放入緩存,隻是設定的緩存過期時間較短,比如設定為 60 s。

這樣第一次不存在也會被加載會記錄,下次拿到有這個key。

完全以緩存為準

更簡單粗暴的,若一個查詢傳回的資料為空,不管是:

  • 資料不存在
  • 還是系統故障

仍緩存該空結果,但其過期時間很短,最長不超過5min。

仍緩存該空結果,但其過期時間很短,最長不超過5min。      

異步更新

使用 延遲異步加載 的政策2,這樣業務前端不會觸發更新,隻有我們資料更新時後端去主動更新。

服務降級

hystrix

互斥鎖(不推薦)

問題根本在于限制處理線程的數量,即key的更新操作添加全局互斥鎖。

在緩存失效時(判斷拿出來的值為空),不是立即去load db,而是

  • 先使用緩存工具的某些帶成功操作傳回值的操作(Redis的SETNX)去set一個mutex key
  • 當操作傳回成功時,再load db的操作并回設緩存;否則,就重試整個get緩存的方法。
public String get(key) {
      String value = redis.get(key);
      if (value == null) { // 緩存已過期
          // 設定逾時,防止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;      
        }
 }
      

提前"使用互斥鎖(不推薦)

在value内部設定1個逾時值(timeout1), timeout1比實際的memcache timeout(timeout2)小。當從cache讀取到timeout1發現它已經過期時候,馬上延長timeout1并重新設定到cache。然後再從資料庫加載資料并設定到cache中。僞代碼如下:

v = memcache.get(key);  
if (v == null) {  
    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();  
    }  
} else {  
    if (v.timeout <= now()) {  
        if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {  
            // extend the timeout for other threads  
            v.timeout += 3 * 60 * 1000;  
            memcache.set(key, v, KEY_TIMEOUT * 2);  
  
            // load the latest value from db  
            v = db.get(key);  
            v.timeout = KEY_TIMEOUT;  
            memcache.set(key, value, KEY_TIMEOUT * 2);  
            memcache.delete(key_mutex);  
        } else {  
            sleep(50);  
            retry();  
        }  
    }  
}