一文讀懂緩存雪崩、緩存擊穿、緩存穿透及其解決方案

存儲矩陣 發佈 2022-12-05T14:13:37.882148+00:00

使用緩存並沒有這麼簡單,引入了緩存層,就會有緩存異常的三個問題,分別是緩存雪崩、緩存擊穿、緩存穿透。

說明:本文的部分圖片來自於網絡,內容為自己的整理和理解。

背景

Redis 作為目前使用最廣泛的緩存,搭配MySQL的使用場景相信大家都不陌生。因為 Redis 是內存資料庫,我們可以將資料庫的數據緩存在 Redis 里,相當於數據緩存在內存,內存的讀寫速度比硬碟快好幾個數量級,這樣大大提高了系統性能。使用緩存並沒有這麼簡單,引入了緩存層,就會有緩存異常的三個問題,分別是緩存雪崩、緩存擊穿、緩存穿透那什麼是緩存雪崩,擊穿,穿透呢?出現這些問題又怎麼解決呢?

下面以常見的Redis緩存組件為例來講解這三種場景及解決方案。帶領大家逐一解答這些問題。

示例緩存架構

TMC,即「透明多級緩存(Transparent Multilevel Cache)」,是有贊 PaaS 團隊給公司內應用提供的整體緩存解決方案。

TMC 整體架構如上圖,共分為三層:

  • 存儲層:提供基礎的 kv 數據存儲能力,針對不同的業務場景選用不同的存儲服務(codis/zankv/aerospike);
  • 代理層:為應用層提供統一的緩存使用入口及通信協議,承擔分布式數據水平切分後的路由功能轉發工作;
  • 應用層:提供統一客戶端給應用服務使用,內置「熱點探測」、「本地緩存」等功能,對業務透明;

當我們使用緩存時,目標通常有兩個:第一,提升響應效率和並發量;第二,減輕資料庫的壓力。而本文中所提到的這三種場景:緩存穿透、緩存雪崩和緩存擊穿的發生,都是因為在某些特殊情況下,緩存失去了預期的功能所致。

當緩存失效或沒有抵擋住流量,流量直接湧入到資料庫,在高並發的情況下,可能直接擊垮資料庫,導致整個系統崩潰。

這就是我們需要知道的大前提,而緩存穿透、緩存雪崩和緩存擊穿,只不過是在這個大前提下的不同場景的細分場景而已。

緩存雪崩

大量的應用請求無法在Redis緩存中進行處理(比如:為了保證緩存中的數據與資料庫中的數據一致性,會給 Redis 里的數據設置過期時間,當緩存數據過期後,用戶訪問的數據如果不在緩存里,業務系統需要重新生成緩存),緊接著應用將大量請求發送到資料庫層,導致資料庫層的壓力激增。

大量緩存數據在同一時間過期(失效)或者 Redis 故障宕機時,如果此時有大量的用戶請求進來,因為Redis不可服務,全部請求都直接訪問資料庫,從而導致資料庫的壓力驟增,嚴重的會造成資料庫宕機,這就是緩存雪崩

可以看到,發生緩存雪崩有多個原因:

原因一:緩存中有大量Key同時過期,導致大量請求無法得到處理,大量數據需要回源資料庫

  • 方案一 差異化設置過期時間:差異化緩存過期時間,不要讓大量的 Key 在同一時間過期。比如,在初始化緩存的時候,給這些數據的過期時間增加一個較小的隨機數,這樣一來不同數據的過期時間有所差別又差別不大,即避免了大量數據同時過期又能保證這些數據在相近的時間失效
  • 方案二服務降級:允許核心業務訪問資料庫,非核心業務直接返回預定義的信息
  • 方案三後台更新緩存,不設置過期時間:初始化緩存數據的時候設置緩存永不過期,然後啟動一個後台線程 30 秒一次定時把所有數據更新到緩存,而且通過適當的休眠,控制從資料庫更新數據的頻率,降低資料庫壓力。事實上,緩存數據不設置有效期,並不是意味著數據一直能在內存里,因為當系統內存緊張的時候,有些緩存數據會被「淘汰」,而在緩存被「淘汰」到下一次後台定時更新緩存的這段時間內,業務線程讀取緩存失敗就返回空值,業務的視角就以為是數據丟失了。解決上面的問題的方式有兩種。第一種方式,後台線程不僅負責定時更新緩存,而且也負責頻繁地檢測緩存是否有效,檢測到緩存失效了,原因可能是系統緊張而被淘汰的,於是就要馬上從資料庫讀取數據,並更新到緩存。這種方式的檢測時間間隔不能太長,太長也導致用戶獲取的數據是一個空值而不是真正的數據,所以檢測的間隔最好是毫秒級的,但是總歸是有個間隔時間,用戶體驗一般。第二種方式,在業務線程發現緩存數據失效後(緩存數據被淘汰),通過消息隊列發送一條消息通知後台線程更新緩存,後台線程收到消息後,在更新緩存前可以判斷緩存是否存在,存在就不執行更新緩存操作;不存在就讀取資料庫數據,並將數據加載到緩存。這種方式相比第一種方式緩存的更新會更及時,用戶體驗也比較好。在業務剛上線的時候,我們最好提前把數據緩起來,而不是等待用戶訪問才來觸發緩存構建,這就是所謂的緩存預熱,後台更新緩存的機制剛好也適合幹這個事情。
  • 方案四互斥鎖當業務線程在處理用戶請求時,如果發現訪問的數據不在 Redis 里,就加個互斥鎖,保證同一時間內只有一個請求來構建緩存(從資料庫讀取數據,再將數據更新到 Redis 里),當緩存構建完成後,再釋放鎖。未能獲取互斥鎖的請求,要麼等待鎖釋放後重新讀取緩存,要麼就返回空值或者默認值。實現互斥鎖的時候,最好設置超時時間,不然第一個請求拿到了鎖,然後這個請求發生了某種意外而一直阻塞,一直不釋放鎖,這時其他請求也一直拿不到鎖,整個系統就會出現無響應的現象。
  • 方案四雙key策略,主key設置過期時間,備key不設置過期時間,當主key失效時,直接返回備key值。

原因二:Redis實例發生故障宕機,無法處理請求,就會導致大量請求積壓到資料庫層

  • 方案一 服務熔斷:暫停業務應用對緩存服務的訪問,從而降低對資料庫的壓力
  • 方案二 請求限流:控制每秒進入應用程式的請求數,避免過多的請求被發到資料庫
  • 方案三 Redis構建高可靠集群:通過主從節點的方式構建Redis高可靠集群。可以保證在Redis主節點故障宕機時,從節點切換到主節點,繼續提供服務,避免由於緩存實例宕機導致緩存雪崩

緩存擊穿

緩存擊穿是指熱點key在某個時間點過期的時候,而恰好在這個時間點對這個Key有大量的並發請求過來,從而大量的請求打到db,屬於常見的「熱點」問題

如果緩存中的某個熱點數據過期了,此時大量的請求訪問了該熱點數據,就無法從緩存中讀取,直接訪問資料庫,資料庫很容易就被高並發的請求衝垮,這就是緩存擊穿的問題。

可以發現緩存擊穿跟緩存雪崩很相似,你可以認為緩存擊穿是緩存雪崩的一個子集。應對緩存擊穿可以採取前面說到兩種方案:

  1. 預先設置熱門數據,提前存入緩存
  2. 實時監控熱門數據,調整key過期時長
  3. 二級緩存:對於熱點數據進行二級緩存,並對於不同級別的緩存設定不同的失效時間。
  4. 設置分布式鎖:保證同一時間只有一個業務線程更新緩存,未能獲取互斥鎖的請求,要麼等待鎖釋放後重新讀取緩存,要麼就返回空值或者默認值。

緩存穿透

緩存穿透是指客戶端請求的數據既不在緩存中,也不在資料庫中,這樣緩存永遠不會生效,這些請求都會被打倒資料庫上。

即這個數據根本不存在,如果黑客攻擊時,啟用很多個線程,一直對這個不存在的數據發送請求 ,那麼請求就會一直被打到資料庫上,很容易將資料庫打崩。


緩存穿透的發生一般有這兩種情況:

  • 業務誤操作,緩存中的數據和資料庫中的數據都被誤刪除了,所以導致緩存和資料庫中都沒有數據;
  • 黑客惡意攻擊,故意大量訪問某些讀取不存在數據的業務;

應對緩存穿透的方案,常見的方案有三種。

  • 第一種方案非法請求的限制(使用bitmaps類型定義訪問白名單,或進行實時監控,和運維人員配合排查訪問對象和訪問數據設置黑名單限制服務):當有大量惡意請求訪問不存在的數據的時候,也會發生緩存穿透,因此在 API 入口處我們要判斷求請求參數是否合理,請求參數是否含有非法值、請求欄位是否存在,如果判斷出是惡意請求就直接返回錯誤,避免進一步訪問緩存和資料庫。
  • 第二種方案緩存空值或者默認值;當我們線上業務發現緩存穿透的現象時,可以針對查詢的數據,在緩存中設置一個空值或者默認值,這樣後續請求就可以從緩存中讀取到空值或者默認值,返回給應用,而不會繼續查詢資料庫。
  • 第三種方案使用布隆過濾器快速判斷數據是否存在,避免通過查詢資料庫來判斷數據是否存在;我們可以在寫入資料庫數據時,使用布隆過濾器做個標記,然後在用戶請求到來時,業務線程確認緩存失效後,可以通過查詢布隆過濾器快速判斷數據是否存在,如果不存在,就不用通過查詢資料庫來判斷數據是否存在。即使發生了緩存穿透,大量請求只會查詢 Redis 和布隆過濾器,而不會查詢資料庫,保證了資料庫能正常運行,Redis 自身也是支持布隆過濾器的。

布隆過濾器的原理:由「初始值都為 0 的位圖數組」和「 N 個哈希函數」兩部分組成。當我們在寫入資料庫數據時,在布隆過濾器里做個標記,這樣下次查詢數據是否在資料庫時,只需要查詢布隆過濾器,如果查詢到數據沒有被標記,說明不在資料庫中,但是如果查到了,因為hash衝突也不能說明數據一定在資料庫中。

一張圖總結:

擊穿與雪崩的區別即在於擊穿是對於特定的熱點數據來說,而雪崩是全部數據。

緩存異常會面臨的三個問題:緩存雪崩、擊穿和穿透。

其中,緩存雪崩和緩存擊穿主要原因是數據不在緩存中,而導致大量請求訪問了資料庫,資料庫壓力驟增,容易引發一系列連鎖反應,導致系統奔潰。不過,一旦數據被重新加載回緩存,應用又可以從緩存快速讀取數據,不再繼續訪問資料庫,資料庫的壓力也會瞬間降下來。因此,緩存雪崩和緩存擊穿應對的方案比較類似。

而緩存穿透主要原因是數據既不在緩存也不在資料庫中。因此,緩存穿透與緩存雪崩、擊穿應對的方案不太一樣。

關鍵字: