一張圖搞懂 Redis 緩存雪崩、緩存穿透、緩存擊穿
作者 | 雷架
文章來源 | 愛笑的架構師 (id :DancingOnYourCode)
緩存異常場景分類
在實際生產環境中有時會遇到緩存穿透、緩存擊穿、緩存雪崩等異常場景,爲了避免異常帶來巨大損失,我們需要了解每種異常發生的原因以及解決方案,幫助提升系統可靠性和高可用。
緩存穿透
什麼是緩存穿透?
緩存穿透是指用戶請求的數據在緩存中不存在即沒有命中,同時在數據庫中也不存在,導致用戶每次請求該數據都要去數據庫中查詢一遍,然後返回空。
如果有惡意攻擊者不斷請求系統中不存在的數據,會導致短時間大量請求落在數據庫上,造成數據庫壓力過大,甚至擊垮數據庫系統。
緩存穿透常用的解決方案
(1)布隆過濾器(推薦)
布隆過濾器(Bloom Filter,簡稱BF)由Burton Howard Bloom在1970年提出,是一種空間效率高的概率型數據結構。
布隆過濾器專門用來檢測集合中是否存在特定的元素。
如果在平時我們要判斷一個元素是否在一個集合中,通常會採用查找比較的方法,下面分析不同的數據結構查找效率:
採用線性表存儲,查找時間複雜度爲O(N)
採用平衡二叉排序樹(AVL、紅黑樹)存儲,查找時間複雜度爲O(logN)
採用哈希表存儲,考慮到哈希碰撞,整體時間複雜度也要O[log(n/m)]
當需要判斷一個元素是否存在於海量數據集合中,不僅查找時間慢,還會佔用大量存儲空間。接下來看一下布隆過濾器如何解決這個問題。
布隆過濾器設計思想
布隆過濾器由一個長度爲m比特的位數組(bit array)與k個哈希函數(hash function)組成的數據結構。位數組初始化均爲0,所有的哈希函數都可以分別把輸入數據儘量均勻地散列。
當要向布隆過濾器中插入一個元素時,該元素經過k個哈希函數計算產生k個哈希值,以哈希值作爲位數組中的下標,將所有k個對應的比特值由0置爲1。
當要查詢一個元素時,同樣將其經過哈希函數計算產生哈希值,然後檢查對應的k個比特值:如果有任意一個比特爲0,表明該元素一定不在集合中;如果所有比特均爲1,表明該集合有可能性在集合中。爲什麼不是一定在集合中呢?因爲不同的元素計算的哈希值有可能一樣,會出現哈希碰撞,導致一個不存在的元素有可能對應的比特位爲1,這就是所謂“假陽性”(false positive)。相對地,“假陰性”(false negative)在BF中是絕不會出現的。
總結一下:布隆過濾器認爲不在的,一定不會在集合中;布隆過濾器認爲在的,可能在也可能不在集合中。
舉個例子:下圖是一個布隆過濾器,共有18個比特位,3個哈希函數。集合中三個元素x,y,z通過三個哈希函數散列到不同的比特位,並將比特位置爲1。當查詢元素w時,通過三個哈希函數計算,發現有一個比特位的值爲0,可以肯定認爲該元素不在集合中。
布隆過濾器優缺點
優點:
節省空間:不需要存儲數據本身,只需要存儲數據對應hash比特位
時間複雜度低:插入和查找的時間複雜度都爲O(k),k爲哈希函數的個數
缺點:
存在假陽性:布隆過濾器判斷存在,可能出現元素不在集合中;判斷準確率取決於哈希函數的個數
不能刪除元素:如果一個元素被刪除,但是卻不能從布隆過濾器中刪除,這也是造成假陽性的原因了
布隆過濾器適用場景
爬蟲系統url去重
垃圾郵件過濾
黑名單
(2)返回空對象
當緩存未命中,查詢持久層也爲空,可以將返回的空對象寫到緩存中,這樣下次請求該key時直接從緩存中查詢返回空對象,請求不會落到持久層數據庫。爲了避免存儲過多空對象,通常會給空對象設置一個過期時間。
這種方法會存在兩個問題:
如果有大量的key穿透,緩存空對象會佔用寶貴的內存空間。
空對象的key設置了過期時間,在這段時間可能會存在緩存和持久層數據不一致的場景。
緩存擊穿
什麼是緩存擊穿?
緩存擊穿,是指一個key非常熱點,在不停的扛着大併發,大併發集中對這一個點進行訪問,當這個key在失效的瞬間,持續的大併發就穿破緩存,直接請求數據庫,就像在一個屏障上鑿開了一個洞。
緩存擊穿危害
數據庫瞬時壓力驟增,造成大量請求阻塞。
如何解決
使用互斥鎖(mutex key)
這種思路比較簡單,就是讓一個線程回寫緩存,其他線程等待回寫緩存線程執行完,重新讀緩存即可。
同一時間只有一個線程讀數據庫然後回寫緩存,其他線程都處於阻塞狀態。如果是高併發場景,大量線程阻塞勢必會降低吞吐量。這種情況如何解決?大家可以在留言區討論。
如果是分佈式應用就需要使用分佈式鎖。
熱點數據永不過期
永不過期實際包含兩層意思:
物理不過期,針對熱點key不設置過期時間
邏輯過期,把過期時間存在key對應的value裏,如果發現要過期了,通過一個後臺的異步線程進行緩存的構建
緩存雪崩
什麼是緩存雪崩?
緩存雪崩是指緩存中數據大批量到過期時間,而查詢數據量巨大,請求直接落到數據庫上,引起數據庫壓力過大甚至宕機。和緩存擊穿不同的是,緩存擊穿指併發查同一條數據,緩存雪崩是不同數據都過期了,很多數據都查不到從而查數據庫。
緩存雪崩解決方案
常用的解決方案有:
均勻過期
加互斥鎖
緩存永不過期
雙層緩存策略
(1)均勻過期
設置不同的過期時間,讓緩存失效的時間點儘量均勻。通常可以爲有效期增加隨機值或者統一規劃有效期。
(2)加互斥鎖
跟緩存擊穿解決思路一致,同一時間只讓一個線程構建緩存,其他線程阻塞排隊。
(3)緩存永不過期
跟緩存擊穿解決思路一致,緩存在物理上永遠不過期,用一個異步的線程更新緩存。
(4)雙層緩存策略
使用主備兩層緩存:
主緩存:有效期按照經驗值設置,設置爲主讀取的緩存,主緩存失效後從數據庫加載最新值。
備份緩存:有效期長,獲取鎖失敗時讀取的緩存,主緩存更新時需要同步更新備份緩存。
緩存預熱什麼是緩存預熱?
緩存預熱就是系統上線後,將相關的緩存數據直接加載到緩存系統,這樣就可以避免在用戶請求的時候,先查詢數據庫,然後再將數據回寫到緩存。
如果不進行預熱, 那麼 Redis 初始狀態數據爲空,系統上線初期,對於高併發的流量,都會訪問到數據庫中, 對數據庫造成流量的壓力。
緩存預熱的操作方法
數據量不大的時候,工程啓動的時候進行加載緩存動作;
數據量大的時候,設置一個定時任務腳本,進行緩存的刷新;
數據量太大的時候,優先保證熱點數據進行提前加載到緩存。
緩存降級
緩存降級是指緩存失效或緩存服務器掛掉的情況下,不去訪問數據庫,直接返回默認數據或訪問服務的內存數據。
在項目實戰中通常會將部分熱點數據緩存到服務的內存中,這樣一旦緩存出現異常,可以直接使用服務的內存數據,從而避免數據庫遭受巨大壓力。
降級一般是有損的操作,所以儘量減少降級對於業務的影響程度。