作者 | 雷架

文章來源 | 愛笑的架構師 (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 初始狀態數據爲空,系統上線初期,對於高併發的流量,都會訪問到數據庫中, 對數據庫造成流量的壓力。

緩存預熱的操作方法

數據量不大的時候,工程啓動的時候進行加載緩存動作;

數據量大的時候,設置一個定時任務腳本,進行緩存的刷新;

數據量太大的時候,優先保證熱點數據進行提前加載到緩存。

緩存降級

緩存降級是指緩存失效或緩存服務器掛掉的情況下,不去訪問數據庫,直接返回默認數據或訪問服務的內存數據。

在項目實戰中通常會將部分熱點數據緩存到服務的內存中,這樣一旦緩存出現異常,可以直接使用服務的內存數據,從而避免數據庫遭受巨大壓力。

降級一般是有損的操作,所以儘量減少降級對於業務的影響程度。

相關文章