聊一聊Redis熱點key儲存問題

7small7發表於2022-05-16

說明

快取穿透、快取擊穿和快取雪崩是Redis面試當中和實際開發中,經常需要考慮的一個問題。很多人對該問題的產生、原因和解決方案還是不夠清晰。其實大家針對該三種情況,去仔細分析一個產生的原理就能很好的找到一個好的解決方案。

本文通過定義、案例、危害和解決方案的幾個角度,來幫助你快速瞭解該三個問題。

相信大家在網上也看到很多解決這三種問題的解決方案,其中的一些方案是否是一個正確的方案呢?本文也將一一分析此類方案的優缺點。

下圖為本文的內容大綱,文章也是圍繞這幾點進行分析與總結。

三者比較

  1. 快取穿透、快取擊穿和快取雪崩都是因為快取中資料不存在,導致走資料庫去查詢資料。

  2. 由於快取資料不存在,所有的請求都會走到資料庫,因此會導致資料庫的壓力過大甚至出現服務崩潰,導致整個系統無法使用。

快取穿透

定義:快取穿透是由於客戶端求的資料在快取中不存在,然後去查詢資料庫,然而資料庫沒有客戶端要查詢的資料,導致每一次請求都會走資料庫查詢操作。真正的問題在於該資料本身就是不存在的

舉例:客戶端請求商品詳情資訊時,攜帶一個商品ID,此時該商品ID是不存在的(不管是快取中還是資料庫中)。導致每一次請求該ID商品的資料資訊都會走資料庫。

危害:由於請求的引數對應的資料根本不存在,會導致每一次都會請求資料庫,增加資料庫的壓力或者服務崩潰,更有甚至影響到其他的業務模組。經常發生在使用者惡意請求的情況下會發生。

解決方案:

  1. 根據請求的引數快取一個null值。並且為該值設定一個過期時間,可以將時間設定短暫一點。

  2. 使用布隆過濾器,首先通過布隆過濾器進行篩選,如果在過濾器中存在則去查詢資料庫,然後新增到快取中。如果不存在則直接返回客戶端資料不存在。

  3. 由於快取穿透可能是使用者發起惡意請求,可以將使用者ip給記錄下來,針對惡意的ip請求進行封禁。

方案分析:

  1. 第一種方案,針對不存在的key,會快取一個空的值。假設這樣的請求特別多,是否都會一一去設定一個空值的快取,此時Redis中就存在大量無效的快取空值。假設這樣的key是商品或者文章類的ID,我們在設定空值之後,如果後臺新增資料應該去更新ID對應的快取值,並設定一個合理的過期時間。

  2. 第二種方案,也是業界使用最多的一種方案。布隆過濾器的優點在於基於Redis實現,記憶體操作並且底層的實現也是非常節約記憶體。關於布隆過濾器的介紹,可以參考該文章。 當後臺新增資料成功時,將該資料的ID新增到布隆過濾器中,前端在請求時先走布隆過濾器進行驗證是否存在。但布隆過濾器也存在一個弊端,就是hash衝突問題。這裡的hash衝突是什麼意思呢?就是說多個ID在進行hash計算時,得到的hash位都是同一個值,這就導致在驗證是否存在時誤判。本身是有的,得到的結果是沒有。布隆過濾器的一個弊端就是,它說有並不一定有,它說沒有就一點是沒有的。

  3. 第三種方案,針對同一使用者一段時間內發起大量的請求,觸發快取穿透機制,此時我們可以顯示該客戶端的訪問。但攻擊者如果是發起DDOS這樣的攻擊,是沒法完全的避免此類攻擊,因此這種方案不是一個很好的解決方案。

方案總結:

  1. 我們首先在請求層面增加第3中方案,做一個限流機制、IP黑名單機制,控制一些惡意的請求,如果是誤判我們可以實現IP解封這樣的操作。在快取層則使用第1中方案實現。設定一個合理的快取時間。

  2. 對於能容忍誤判的業務場景,可以直接才用第2中方案實現。完全基於Redis,減少了系統的複雜度。

快取擊穿

定義:快取擊穿是因為某個熱點key不存在,導致走資料庫查詢。增加了資料庫的壓力。這種壓力可能是瞬間的,也可能是比較持久的。真正的問題在於該key是存在,只是快取中不存在,導致走資料庫操作

舉例:有一個熱門的商品,使用者檢視商品詳情時攜帶商品的ID以獲取到商品的詳情資訊。此時快取中的資料已經過期了,因此來的所有請求都要走資料庫去查詢。

危害:相對快取穿透而言,該資料在資料庫中是存在的,只是因為快取過期了,導致要走一次資料庫,然後在新增到快取中,下次請求就能正常走快取。所謂的危害同樣的還是針對資料庫層面的危害。

解決方案:

  1. 加互斥鎖。針對第一個請求,發現快取中沒有資料,此時查詢資料庫新增到快取裡面。這樣後面的請求就不需要走資料庫查詢。

  2. 增加業務邏輯過期時間。在設定快取時,我們可以新增一個快取過期時間。每次去讀取的時候,做一個判斷,如果這個過期時間與當前時間小於一個範圍,觸發一個後臺執行緒,去資料庫拉取一下資料,接著更新一下快取資料和快取的過期時間。其實原理就是程式碼層面給快取延長快取時長。

  3. 資料預熱。實現通過後臺把資料新增到快取裡面。例如秒殺場景開始前,就把商品的庫存新增到快取裡面,這樣使用者請求來了之後,就直接走快取。

  4. 永久不過期。在給快取設定過期時間時,讓它永久不過期。後臺單獨開啟一個執行緒,來維護這些快取的過期時間和資料更新。

方案分析:

  1. 互斥鎖保證了只有一個請求走資料庫,這是一個優點。但是對於分散式的系統,得才用分散式鎖實現,分散式鎖的實現本身就有一定的難點,這樣提升了系統的複雜度。關於Redis分散式的介紹,可以參考該文章

  2. 第2種方案,利用Redis不過期,業務過期的方案實現。保證了每一次請求都能拿到資料,同時也可以做到一個後臺執行緒去更新資料。缺點在於後臺執行緒沒有更新完資料,此時請求拿到的資料是舊資料,可能對應實時性要求高的業務場景存在弊端。

  3. 第3種方案,使用快取預熱每次載入都走快取,與第2種方案差不多。不過也存在熱點資料更新問題,因此該方案適合資料實時性要求不高的資料。

  4. 第4中方案,和第2、3種方案類似,在此基礎上進行了一定優化,使用後臺非同步執行緒主動去更新快取資料。難點在於更新的頻率控制。

方案總結:

  1. 對於實時性要求高的資料,推薦使用第1種方案,雖然在技術上有一定的難度但是能做到資料的實時性處理。如果發生某些請求等待時間久,可以返回異常,讓客戶端重新傳送一次請求。

  2. 對於實時性要求不高的資料,可以使用第4種方案。

快取雪崩

定義:前面在說到快取擊穿,是因為快取中的某個熱點key失效,導致大量請求走資料庫。然而快取雪崩其實也是同樣的道理,只不過這個更嚴重而已,是大部分快取的key失效,而不是一個或者兩個key失效。

舉例:在一個電商系統中,某一個分類下的商品資料在快取中都失效了。然而當前系統的很多請求都是該分類下面的商品資料。這樣就導致所有的請求都走資料庫查詢。

危害:由於一瞬間大量的請求湧入,每一個請求都要走資料庫進行查詢。資料庫瞬間流量湧入,嚴重增加資料庫負擔,很容易導致資料庫直接癱瘓。

解決方案:

  1. 快取時間隨機。因為某一時間,大量的快取失效,說明快取的過期時間比較集中。我們直接將過期的時間設定為不集中,隨機打亂。這樣快取過期時間相對不會很集中,就不會出現同一時刻大量請求走資料庫進行查詢操作。

  2. 多級快取。不單純的靠Redis來做快取,我們也可以使用memcached來做快取(這裡只是舉一個例子,其他的快取服務也可以)。快取資料時,對Redis做一個快取,對memcached做一個快取。如果Redis失效了,我們可以走memcached。

  3. 互斥鎖。快取擊穿中我們提到了使用互斥鎖來實現,同樣我們也可以用在雪崩的情況下。

  4. 設定過期標誌。其實也可以用到快取擊穿中講到的永久不過期。當請求時,判斷過期時間,如果臨近過期時間則設定一個過期標誌,觸發一個獨立的執行緒去對這個快取進行更新。

方案分析:

  1. 第1種方案採用隨機數快取時間,能保證key的失效時間分散。難點在於如何設定快取時間,如果對於一些需要設定短快取時間並資料量非常大的資料,該方案就需要合理的控制時間。

  2. 第2種方案使用多級快取,可以保證請求全部走快取資料。但這樣增加了系統的架構難度,以及其他的各種問題,例如快取多級更新。

  3. 第3種方案使用互斥鎖,在快取擊穿中我們提到了互斥鎖,在雪崩的場景中我們雖然能使用,但是這樣會產生大量的分散式鎖。

  4. 第4種方案使用邏輯快取時間,很好的保證了系統的快取壓力。

方案總結:

在實際的專案中推薦使用第1、2和4種方案試下會更好一些。

總結

  1. 快取穿透是因為資料庫本身沒有該資料。

  2. 快取擊穿和快取雪崩是資料庫中存在該資料,只是快取中的資料失效了,導致重新要查詢一次資料庫再新增到快取中去。

  3. 快取擊穿是針對部分熱點key,而快取雪崩是大面積快取失效。兩則原理上其實是一樣的,無非就是針對快取的key的劃分不同而已。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章