緩存穿透、緩存擊穿、緩存雪崩解決方案
前言
我一個QPS不到10的項目,天天問我緩存穿透、緩存擊穿、緩存雪崩,我是真滴難。

可能大家經(jīng)常會有這種感受,但是只要是面試要問的題目,就算用不上,我們也要去學習和了解,誰叫我們窮了。

正文
緩存穿透
描述:訪問一個緩存和數(shù)據(jù)庫都不存在的 key,此時會直接打到數(shù)據(jù)庫上,并且查不到數(shù)據(jù),沒法寫緩存,所以下一次同樣會打到數(shù)據(jù)庫上。
此時,緩存起不到作用,請求每次都會走到數(shù)據(jù)庫,流量大時數(shù)據(jù)庫可能會被打掛。此時緩存就好像被“穿透”了一樣,起不到任何作用。
解決方案:
1、接口校驗。在正常業(yè)務流程中可能會存在少量訪問不存在 key 的情況,但是一般不會出現(xiàn)大量的情況,所以這種場景最大的可能性是遭受了非法攻擊。可以在最外層先做一層校驗:用戶鑒權(quán)、數(shù)據(jù)合法性校驗等,例如商品查詢中,商品的ID是正整數(shù),則可以直接對非正整數(shù)直接過濾等等。
2、緩存空值。當訪問緩存和DB都沒有查詢到值時,可以將空值寫進緩存,但是設(shè)置較短的過期時間,該時間需要根據(jù)產(chǎn)品業(yè)務特性來設(shè)置。
3、布隆過濾器。使用布隆過濾器存儲所有可能訪問的 key,不存在的 key 直接被過濾,存在的 key 則再進一步查詢緩存和數(shù)據(jù)庫。
布隆過濾器
布隆過濾器的特點是判斷不存在的,則一定不存在;判斷存在的,大概率存在,但也有小概率不存在。并且這個概率是可控的,我們可以讓這個概率變小或者變高,取決于用戶本身的需求。
布隆過濾器由一個 bitSet 和 一組 Hash 函數(shù)(算法)組成,是一種空間效率極高的概率型算法和數(shù)據(jù)結(jié)構(gòu),主要用來判斷一個元素是否在集合中存在。
在初始化時,bitSet 的每一位被初始化為0,同時會定義 Hash 函數(shù),例如有3組 Hash 函數(shù):hash1、hash2、hash3。
寫入流程
當我們要寫入一個值時,過程如下,以“jionghui”為例:
1)首先將“jionghui”跟3組 Hash 函數(shù)分別計算,得到 bitSet 的下標為:1、7、10。
2)將 bitSet 的這3個下標標記為1。
假設(shè)我們還有另外兩個值:java 和 diaosi,按上面的流程跟 3組 Hash 函數(shù)分別計算,結(jié)果如下:
java:Hash 函數(shù)計算 bitSet 下標為:1、7、11
diaosi:Hash 函數(shù)計算 bitSet 下標為:4、10、11

查詢流程
當我們要查詢一個值時,過程如下,同樣以“jionghui”為例::
1)首先將“jionghui”跟3組 Hash 函數(shù)分別計算,得到 bitSet 的下標為:1、7、10。
2)查看 bitSet 的這3個下標是否都為1,如果這3個下標不都為1,則說明該值必然不存在,如果這3個下標都為1,則只能說明可能存在,并不能說明一定存在。
其實上圖的例子已經(jīng)說明了這個問題了,當我們只有值“jionghui”和“diaosi”時,bitSet 下標為1的有:1、4、7、10、11。
當我們又加入值“java”時,bitSet 下標為1的還是這5個,所以當 bitSet 下標為1的為:1、4、7、10、11 時,我們無法判斷值“java”存不存在。
其根本原因是,不同的值在跟 Hash 函數(shù)計算后,可能會得到相同的下標,所以某個值的標記位,可能會被其他值給標上了。
這也是為啥布隆過濾器只能判斷某個值可能存在,無法判斷必然存在的原因。但是反過來,如果該值根據(jù) Hash 函數(shù)計算的標記位沒有全部都為1,那么則說明必然不存在,這個是肯定的。
降低這種誤判率的思路也比較簡單:
1)一個是加大 bitSet 的長度,這樣不同的值出現(xiàn)“沖突”的概率就降低了,從而誤判率也降低。
2)提升 Hash 函數(shù)的個數(shù),Hash 函數(shù)越多,每個值對應的 bit 越多,從而誤判率也降低。
布隆過濾器的誤判率還有專門的推導公式,有興趣的可以去搜相關(guān)的文章和論文查看。
HashMap 和 布隆過濾器
估計有同學看了上面的例子,會覺得使用 HashMap 也能實現(xiàn)。
確實,當數(shù)據(jù)量不大時,HashMap 實現(xiàn)起來一點問題都沒有,而且還沒有誤判率,簡直完美,還要個雞兒布隆過濾器。

不過,當數(shù)據(jù)量上去后,布隆過濾器的空間優(yōu)勢就會開始體現(xiàn),特別是要存儲的 key 占用空間越大,布隆過濾器的優(yōu)勢越明顯。
Guava 中的 BloomFilter 在默認情況下,誤判率接近3%,大概要使用5個 Hash 函數(shù)。
也就是說一個 key 最多占用空間就是 5 bit,而且當多個 key 填充同一個 bit 時,會進一步降低使用空間。
布隆過濾器占用多少空間,主要取決于 Hash 函數(shù)的個數(shù),跟 key 本身的大小無關(guān),這使得其在空間的優(yōu)勢非常大。
緩存擊穿
描述:某一個熱點 key,在緩存過期的一瞬間,同時有大量的請求打進來,由于此時緩存過期了,所以請求最終都會走到數(shù)據(jù)庫,造成瞬時數(shù)據(jù)庫請求量大、壓力驟增,甚至可能打垮數(shù)據(jù)庫。
解決方案:
1、加互斥鎖。在并發(fā)的多個請求中,只有第一個請求線程能拿到鎖并執(zhí)行數(shù)據(jù)庫查詢操作,其他的線程拿不到鎖就阻塞等著,等到第一個線程將數(shù)據(jù)寫入緩存后,直接走緩存。
關(guān)于互斥鎖的選擇,網(wǎng)上看到的大部分文章都是選擇 Redis 分布式鎖(可以參考我之前的文章:面試必問的分布式鎖,你懂了嗎?),因為這個可以保證只有一個請求會走到數(shù)據(jù)庫,這是一種思路。
但是其實仔細想想的話,這邊其實沒有必要保證只有一個請求走到數(shù)據(jù)庫,只要保證走到數(shù)據(jù)庫的請求能大大降低即可,所以還有另一個思路是 JVM 鎖。
JVM 鎖保證了在單臺服務器上只有一個請求走到數(shù)據(jù)庫,通常來說已經(jīng)足夠保證數(shù)據(jù)庫的壓力大大降低,同時在性能上比分布式鎖更好。
需要注意的是,無論是使用“分布式鎖”,還是“JVM 鎖”,加鎖時要按 key 維度去加鎖。
我看網(wǎng)上很多文章都是使用一個“固定的 key”加鎖,這樣會導致不同的 key 之間也會互相阻塞,造成性能嚴重損耗。
使用 redis 分布式鎖的偽代碼,僅供參考:
public Object getData(String key) throws InterruptedException {Object value = redis.get(key);// 緩存值過期if (value == null) {// lockRedis:專門用于加鎖的redis;// "empty":加鎖的值隨便設(shè)置都可以if (lockRedis.set(key, "empty", "PX", lockExpire, "NX")) {try {// 查詢數(shù)據(jù)庫,并寫到緩存,讓其他線程可以直接走緩存value = getDataFromDb(key);redis.set(key, value, "PX", expire);} catch (Exception e) {// 異常處理} finally {// 釋放鎖lockRedis.delete(key);}} else {// sleep50ms后,進行重試Thread.sleep(50);return getData(key);}}return value;}
2、熱點數(shù)據(jù)不過期。直接將緩存設(shè)置為不過期,然后由定時任務去異步加載數(shù)據(jù),更新緩存。
這種方式適用于比較極端的場景,例如流量特別特別大的場景,使用時需要考慮業(yè)務能接受數(shù)據(jù)不一致的時間,還有就是異常情況的處理,不要到時候緩存刷新不上,一直是臟數(shù)據(jù),那就涼了。
緩存雪崩
描述:大量的熱點 key 設(shè)置了相同的過期時間,導在緩存在同一時刻全部失效,造成瞬時數(shù)據(jù)庫請求量大、壓力驟增,引起雪崩,甚至導致數(shù)據(jù)庫被打掛。
緩存雪崩其實有點像“升級版的緩存擊穿”,緩存擊穿是一個熱點 key,緩存雪崩是一組熱點 key。
解決方案:
1、過期時間打散。既然是大量緩存集中失效,那最容易想到就是讓他們不集中生效。可以給緩存的過期時間時加上一個隨機值時間,使得每個 key 的過期時間分布開來,不會集中在同一時刻失效。
2、熱點數(shù)據(jù)不過期。該方式和緩存擊穿一樣,也是要著重考慮刷新的時間間隔和數(shù)據(jù)異常如何處理的情況。
3、加互斥鎖。該方式和緩存擊穿一樣,按 key 維度加鎖,對于同一個 key,只允許一個線程去計算,其他線程原地阻塞等待第一個線程的計算結(jié)果,然后直接走緩存即可。
最后
金三銀四的季節(jié),相信有不少同學正準備跳槽。
我將我最近的原創(chuàng)的文章進行了匯總,其中有不少面試高頻題目解析,很多都是我自己在面試大廠時遇到的,我在對每個題目解析時都會按較高的標準進行深入探討,可能只看一遍并不能完全明白,但是相信反復閱讀,定能有所收獲。
有興趣的同學可以在我公眾號底部的“原創(chuàng)匯總”自行查看。
原創(chuàng)不易,如果你覺得本文寫的還不錯,對你有幫助,請通過【點贊】讓我知道,支持我寫出更好的文章。
推薦閱讀
兩年Java開發(fā)工作經(jīng)驗面試總結(jié)
4 年 Java 經(jīng)驗面試總結(jié)、心得體會
