Redis緩存常見問題——穿透、擊穿、雪崩
Redis最常用來做緩存,它是基于記憶體來操作資料的,當資料量過大時難免會遇到一些緩存問題。這篇文章就來說一說Redis常見的緩存問題。
緩存穿透
我們說當發送請求來查詢某個資料時,首先會從Redis緩存中查詢,如果緩存中有就直接傳回,如果沒有請求才會到資料庫中查詢。而緩存穿透指的就是當通路一個redis緩存和資料庫都不存在的key時,此時這個請求就會直接到資料庫中查詢,但是資料庫中也查不到任何資料,而且也沒有辦法寫緩存。這樣情況緩存相當于沒起作用,當請求量過大時,資料庫很有可能會挂掉。
Redis緩存本來想起到一個“半路攔截”的作用,替資料庫緩解一下壓力,緩存穿透的發生,請求就當Redis緩存不存在,直接穿過了Redis緩存,緩存相當于攔了個寂寞,還是得有資料庫來處理請求。

對于緩存穿透的問題,我們該如何解決?
-
接口校驗
在調用接口時,可以在最外層先做一層校驗,比如使用者鑒權、資料合法性校驗等等,提前過濾掉一些非法的接口請求。例如商品查詢中,商品的ID是正整數,則可以直接對非正整數ID的請求直接過濾掉。
-
緩存空值
當通路緩存和資料庫都沒有的資料時,可以将一個空值寫入緩存,并給這個空值設定較短的過期時間,防止這個無效值一直占有記憶體。
-
布隆過濾器
可以使用布隆過濾器存儲所有可能通路的key,當使用者請求過來,先判斷使用者發來的請求的key是否存在于布隆過濾器中,不存在的key直接被過濾掉,存在的key則進一步查詢緩存和資料庫。布隆過濾器相當于在Redis緩存前面又進行了一次攔截校驗。
布隆過濾器特别使用在海量資料中進行查詢,那它的實作原理到底是什麼?實際上布隆過濾器在Redis中的資料結構就是一個大型的位數組+多個随機映射的哈希函數。
向布隆過濾器中添加key時,會使用多個hash函數計算這個key的hash值,并轉換為數組中對應的索引值,然後對數組長度進行取模運算得到具體的存放位置,每一個hash函數都會計算出不同的位置,然後把數組中對應的位置置為1,就相當于完成添加操作。
當一個請求向布隆過濾器查詢一個key是否存在時,跟添加操作一樣,會把這個要查詢的key通過hash函數計算出數組中對應的索引位置,看看數組中這個位置是否都為1,隻要有一個位置為0,則說明布隆過濾器中不存在這個key。如果這幾個位置都為1,并不能說明這個key一定存在,隻是有非常大機率存在,因為這些位置為1很有可能是因為其它key存在所導緻的。如果這個數組比較稀疏,那麼布隆過濾器判斷正确的機率還是很大的,如果說這個位數組比較密集,那麼判斷正确的機率就會降低。
布隆過濾器空間占用的大小,可能會影響到判斷元素是否存在的精準度。
布隆過濾器有兩個參數:
- 第一個參數是預計元素的數量n
- 第二個參數是錯誤率f。
布隆過濾器的空間占用計算公式根據這兩個參數輸入得到兩個輸出:
- 第一個輸出是數組的長度l,也就是需要的存儲空間大小(bit)
- 第二個輸出是**hash函數的最佳數量k。**hash函數的數量也會直接影響到錯誤率,最佳的數量會有最低的錯誤率。
當實際元素超出預計元素時,錯誤率會上升
布隆過濾器可以過濾查詢redis緩存的請求,防止redis發生緩存穿透。
緩存擊穿
某一個熱點 key,在緩存過期的一瞬間,同時有大量的請求打進來,由于此時緩存過期了,是以請求最終都會走到資料庫,造成瞬時資料庫請求量大、壓力驟增,甚至可能打垮資料庫。
要區分開緩存穿透和緩存擊穿,穿透更像是一把散彈槍,大量的請求瞄準不同的key進行“射擊”。而擊穿,更像是一把AK47沖着牆上一個點進行連續射擊,緩存過期的那一瞬間,相當于牆上被打出一個洞,直接擊穿了Redis這面牆,大量請求查詢同一個key。
對于緩存擊穿該如何解決?
-
加互斥鎖
在并發的多個請求中,隻有第一個請求線程能拿到鎖并執行資料庫查詢操作,其他的線程拿不到鎖就阻塞等着,等到第一個線程将資料寫入緩存後,其它請求直接走緩存查詢資料。
-
熱點資料設定不過期
直接将緩存設定為不過期,然後由定時任務去異步加載資料,更新緩存。這種方式适用于比較極端的場景,例如流量特别特别大的場景。很有可能發生異常,緩存無法更新
緩存雪崩
大量的熱點 key 設定了相同的過期時間,導緻緩存在同一時刻全部失效,造成瞬時資料庫請求量大、壓力驟增,引起雪崩,甚至導緻資料庫被打挂。緩存雪崩其實有點像“更新版的緩存擊穿”,緩存擊穿是一個熱點 key,緩存雪崩是一組熱點 key。
對于緩存雪崩該如何解決?
-
分散過期時間
可以給緩存的過期時間時加上一個随機值時間,使得每個 key 的過期時間分布開來,不會集中在同一時刻失效。
-
熱點資料不過期
該方式和緩存擊穿一樣,也是要着重考慮重新整理的時間間隔和資料異常如何處理的情況。
-
加互斥鎖
該方式和緩存擊穿一樣,按 key 次元加鎖,對于同一個 key,隻允許一個線程去計算,其他線程原地阻塞等待第一個線程的計算結果,然後直接走緩存即可。