天天看點

緩存穿透、緩存擊穿、緩存雪崩的問題與解決方案

緩存概念

在電腦中,高速緩沖存儲器是一個硬體或軟體元件,其存儲資料,以便該資料可以在将來的請求送達更快;存儲在緩存中的資料可能是早期計算的結果,也可能是存儲在其他位置的資料的副本。一個緩存命中時,所請求的資料在高速緩存中找到,而出現高速緩存未命中當它不能發生時發生。緩存命中是通過從緩存中讀取資料來實作的,這比重新計算結果或從速度較慢的資料存儲中讀取要快。是以,從緩存中可以處理的請求越多,系統執行速度就越快。

緩存穿透

緩存穿透的概念很簡單,使用者想要查詢一個資料,發現redis記憶體資料庫沒有,也就是緩存沒有命中,于是向持久層資料庫查詢。發現也沒有,于是本次查詢失敗。當使用者很多的時候,緩存都沒有命中,于是都去請求了持久層資料庫。這會給持久層資料庫造成很大的壓力,這時候就相當于出現了緩存穿透。

想象一下這個情況,如果傳入的參數為-1,會是怎麼樣?這個-1,就是一定不存在的對象。就會每次都去查詢資料庫,而每次查詢都是空,每次又都不會進行緩存。假如有惡意攻擊,就可以利用這個漏洞,對資料庫造成壓力,甚至壓垮資料庫。即便是采用UUID,也是很容易找到一個不存在的KEY,進行攻擊。

(這裡注意的差別:緩存穿透的概念與緩存擊穿并不相同,下面會列出緩存擊穿的概念)

緩存穿透如何去預防呢?

1)布隆過濾器

布隆過濾器是一種資料結構,垃圾網站和正常網站加起來全世界據統計也有幾十億個。網警要過濾這些垃圾網站,總不能到資料庫裡面一個一個去比較吧,這就可以使用布隆過濾器。假設我們存儲一億個垃圾網站位址。

可以先有一億個二進制比特,然後網警用八個不同的随機數産生器(F1,F2, …,F8) 産生八個資訊指紋(f1, f2, …, f8)。接下來用一個随機數産生器 G 把這八個資訊指紋映射到 1 到1億中的八個自然數 g1, g2, …,g8。最後把這八個位置的二進制全部設定為一。過程如下:

那這個布隆過濾器是如何解決redis中的緩存穿透呢?很簡單首先也是對所有可能查詢的參數以hash形式存儲,當使用者想要查詢的時候,使用布隆過濾器發現不在集合中,就直接丢棄,不再進行緩存查詢。

2、緩存空對象

當存儲層不命中後,即使傳回的空對象也将其緩存起來,同時會設定一個過期時間,之後再通路這個資料将會從緩存中擷取,保護了後端資料源;

這種方法會存在兩個問題(占用空間資源較多、業務一緻性問題):

Object nullValue = new Object();

try {

  Object valueFromDB = getFromDB(uid); //從資料庫中查詢資料

  if (valueFromDB == null) {

    cache.set(uid, nullValue, 10);   //如果從資料庫中查詢到空值,就把空值寫入緩存,設定較短的逾時時間

  } else {

    cache.set(uid, valueFromDB, 1000);

  }

} catch(Exception e) {

  cache.set(uid, nullValue, 10);

}

1.如果空值能夠被緩存起來,這就意味着緩存需要更多的空間存儲更多的鍵,因為這當中可能會有很多的空值的鍵;

即使對空值設定了過期時間,還是會存在緩存層和存儲層的資料會有一段時間視窗的不一緻,這對于需要保持一緻性的業務會有影響。(這個方法需要具體根據業務分析方案可行性。)

緩存擊穿

緩存擊穿,是指一個key非常熱點,在不停的扛着大并發,大并發集中對這一個點進行通路,當這個key在失效的瞬間,持續的大并發就穿破緩存,直接請求資料庫,就像在一個屏障上鑿開了一個洞。 緩存被“擊穿”的問題,(這個和緩存雪崩的差別在于這裡針對某一key緩存,前者則是很多key,這裡是指同一個key。)

緩存擊穿的問題又該如何去預防呢?

資料持久化。設定熱點資料永遠不過期。(這個基于業務去考慮是否熱點資料是可以持久化的操作。)

檢查更新。将緩存key的過期時間(緩存資料存入時寫的過期)一起儲存到緩存中.在每次擷取緩存資料操作後,都将get出來的緩存過期時間與目前系統時間做一個對比,如果緩存過期時間-目前系統時間<=1分鐘(自定義的一個值),則主動更新緩存.這樣就能保證緩存中的資料始終是最新的(和方案一一樣,讓資料不過期.)

使用互斥鎖。

互斥鎖的實作思路是什麼呢?就是當某一熱點資料失效後,通過互斥鎖的機制把所有并發實作一個串行化的管理。隻有拿到鎖的線程,才去資料庫查詢資料更新緩存,其他的線程在緩存中沒有拿到資料且沒有拿倒鎖的狀态下進行等待。這樣很大程度下降低了對DB資料庫的壓力。不好的地方是可以當緩存失效時。會有短暫的等待時間。下面上一個互斥鎖的demo。

// 方法1:

static Lock reenLock = new ReentrantLock();

    public List<String> getData() throws Exception {

        List<String> result = new ArrayList<String>();

        // 從緩存讀取資料

        result = getCacheData();

        if (result.isEmpty()) {

            if (reenLock.tryLock()) {

                try {

                    System.out.println("我拿到鎖了,從DB擷取資料庫後寫入緩存");

                    // 從資料庫查詢資料

                    result = getDataFromDB();

                    // 将查詢到的資料寫入緩存

                    setDataToCache(result);

                } finally {

                    reenLock.unlock();// 釋放鎖

                }

            } else {

                result = getDataFromCache();// 先查一下緩存

                if (result.isEmpty()) {

                    System.out.println("我沒拿到鎖,緩存也沒資料,先進行等待");

                    Thread.sleep(100);// 等待

                    return getData04();// 重試

                }

            }

        }

        return result;

      }

緩存雪崩

緩存雪崩概念是指:緩存層出現了錯誤,不能正常工作了。大量key同一時間點失效,同時又有大量請求打進來,導緻流量直接打在DB上,造成DB也會CPU爆滿挂掉的情況。

那麼如何解決緩存雪崩問題呢?

 redisTemplate.opsForValue().set("2","xxx",2, 随機數);

解決思路1:可以給緩存的失效時間都設定成随機值,或者分析使用者行為。利用程式手段盡量時緩存key值不在同一時間失效。進而減少DB的瞬間壓力(根據具體業務分析)。

解決思路2:設定多級緩存。比如同時使用redis和memcache緩存,請求->redis->memcache->db;這樣可以方式redis緩存同一時間,并發請求不會直接打到DB問題,而是memcache緩存中那去資料。

————————————————

原文連結:https://blog.csdn.net/weixin_42273066/article/details/114371534