再也不怕,緩存雪崩、擊穿、穿透!
當用戶的請求,都訪問數(shù)據(jù)庫的話,請求數(shù)量一上來,數(shù)據(jù)庫很容易就奔潰的了,所以為了避免用戶直接訪問數(shù)據(jù)庫,會用 Redis 作為緩存層。
因為 Redis 是內(nèi)存數(shù)據(jù)庫,我們可以將數(shù)據(jù)庫的數(shù)據(jù)緩存在 Redis 里,相當于數(shù)據(jù)緩存在內(nèi)存,內(nèi)存的讀寫速度比硬盤快好幾個數(shù)量級,這樣大大提高了系統(tǒng)性能。
引入了緩存層,就會有緩存異常的三個問題,分別是緩存雪崩、緩存擊穿、緩存穿透。
這三個問題也是面試中很??疾斓膯栴},我們不光要清楚地知道它們是怎么發(fā)生,還需要知道如何解決它們。
話不多說,發(fā)車!
緩存雪崩
通常我們?yōu)榱吮WC緩存中的數(shù)據(jù)與數(shù)據(jù)庫中的數(shù)據(jù)一致性,會給 Redis 里的數(shù)據(jù)設置過期時間,當緩存數(shù)據(jù)過期后,用戶訪問的數(shù)據(jù)如果不在緩存里,業(yè)務系統(tǒng)需要重新生成緩存,因此就會訪問數(shù)據(jù)庫,并將數(shù)據(jù)更新到 Redis 里,這樣后續(xù)請求都可以直接命中緩存。
那么,當大量緩存數(shù)據(jù)在同一時間過期(失效)或者 Redis 故障宕機時,如果此時有大量的用戶請求,都無法在 Redis 中處理,于是全部請求都直接訪問數(shù)據(jù)庫,從而導致數(shù)據(jù)庫的壓力驟增,嚴重的會造成數(shù)據(jù)庫宕機,從而形成一系列連鎖反應,造成整個系統(tǒng)崩潰,這就是緩存雪崩的問題。
可以看到,發(fā)生緩存雪崩有兩個原因:
-
大量數(shù)據(jù)同時過期;
-
Redis 故障宕機;
不同的誘因,應對的策略也會不同。
大量數(shù)據(jù)同時過期
針對大量數(shù)據(jù)同時過期而引發(fā)的緩存雪崩問題,常見的應對方法有下面這幾種:
-
均勻設置過期時間;
-
互斥鎖;
-
雙 key 策略;
-
后臺更新緩存;
1. 均勻設置過期時間
如果要給緩存數(shù)據(jù)設置過期時間,應該避免將大量的數(shù)據(jù)設置成同一個過期時間。我們可以在對緩存數(shù)據(jù)設置過期時間時,給這些數(shù)據(jù)的過期時間加上一個隨機數(shù),這樣就保證數(shù)據(jù)不會在同一時間過期。
2. 互斥鎖
當業(yè)務線程在處理用戶請求時,如果發(fā)現(xiàn)訪問的數(shù)據(jù)不在 Redis 里,就加個互斥鎖,保證同一時間內(nèi)只有一個請求來構(gòu)建緩存(從數(shù)據(jù)庫讀取數(shù)據(jù),再將數(shù)據(jù)更新到 Redis 里),當緩存構(gòu)建完成后,再釋放鎖。未能獲取互斥鎖的請求,要么等待鎖釋放后重新讀取緩存,要么就返回空值或者默認值。
實現(xiàn)互斥鎖的時候,最好設置超時時間,不然第一個請求拿到了鎖,然后這個請求發(fā)生了某種意外而一直阻塞,一直不釋放鎖,這時其他請求也一直拿不到鎖,整個系統(tǒng)就會出現(xiàn)無響應的現(xiàn)象。
3. 雙 key 策略
我們對緩存數(shù)據(jù)可以使用兩個 key,一個是主 key,會設置過期時間,一個是備 key,不會設置過期,它們只是 key 不一樣,但是 value 值是一樣的,相當于給緩存數(shù)據(jù)做了個副本。
當業(yè)務線程訪問不到「主 key 」的緩存數(shù)據(jù)時,就直接返回「備 key 」的緩存數(shù)據(jù),然后在更新緩存的時候,同時更新「主 key 」和「備 key 」的數(shù)據(jù)。
4. 后臺更新緩存
業(yè)務線程不再負責更新緩存,緩存也不設置有效期,而是讓緩存“永久有效”,并將更新緩存的工作交由后臺線程定時更新。
事實上,緩存數(shù)據(jù)不設置有效期,并不是意味著數(shù)據(jù)一直能在內(nèi)存里,因為當系統(tǒng)內(nèi)存緊張的時候,有些緩存數(shù)據(jù)會被“淘汰”,而在緩存被“淘汰”到下一次后臺定時更新緩存的這段時間內(nèi),業(yè)務線程讀取緩存失敗就返回空值,業(yè)務的視角就以為是數(shù)據(jù)丟失了。
解決上面的問題的方式有兩種。
第一種方式,后臺線程不僅負責定時更新緩存,而且也負責頻繁地檢測緩存是否有效,檢測到緩存失效了,原因可能是系統(tǒng)緊張而被淘汰的,于是就要馬上從數(shù)據(jù)庫讀取數(shù)據(jù),并更新到緩存。
這種方式的檢測時間間隔不能太長,太長也導致用戶獲取的數(shù)據(jù)是一個空值而不是真正的數(shù)據(jù),所以檢測的間隔最好是毫秒級的,但是總歸是有個間隔時間,用戶體驗一般。
第二種方式,在業(yè)務線程發(fā)現(xiàn)緩存數(shù)據(jù)失效后(緩存數(shù)據(jù)被淘汰),通過消息隊列發(fā)送一條消息通知后臺線程更新緩存,后臺線程收到消息后,在更新緩存前可以判斷緩存是否存在,存在就不執(zhí)行更新緩存操作;不存在就讀取數(shù)據(jù)庫數(shù)據(jù),并將數(shù)據(jù)加載到緩存。這種方式相比第一種方式緩存的更新會更及時,用戶體驗也比較好。
在業(yè)務剛上線的時候,我們最好提前把數(shù)據(jù)緩起來,而不是等待用戶訪問才來觸發(fā)緩存構(gòu)建,這就是所謂的緩存預熱,后臺更新緩存的機制剛好也適合干這個事情。
Redis 故障宕機
針對 Redis 故障宕機而引發(fā)的緩存雪崩問題,常見的應對方法有下面這幾種:
-
服務熔斷或請求限流機制;
-
構(gòu)建 Redis 緩存高可靠集群;
1. 服務熔斷或請求限流機制
因為 Redis 故障宕機而導致緩存雪崩問題時,我們可以啟動服務熔斷機制,暫停業(yè)務應用對緩存服務的訪問,直接返回錯誤,不用再繼續(xù)訪問數(shù)據(jù)庫,從而降低對數(shù)據(jù)庫的訪問壓力,保證數(shù)據(jù)庫系統(tǒng)的正常運行,然后等到 Redis 恢復正常后,再允許業(yè)務應用訪問緩存服務。
服務熔斷機制是保護數(shù)據(jù)庫的正常允許,但是暫停了業(yè)務應用訪問緩存服系統(tǒng),全部業(yè)務都無法正常工作
為了減少對業(yè)務的影響,我們可以啟用請求限流機制,只將少部分請求發(fā)送到數(shù)據(jù)庫進行處理,再多的請求就在入口直接拒絕服務,等到 Redis 恢復正常并把緩存預熱完后,再解除請求限流的機制。
2. 構(gòu)建 Redis 緩存高可靠集群
服務熔斷或請求限流機制是緩存雪崩發(fā)生后的應對方案,我們最好通過主從節(jié)點的方式構(gòu)建 Redis 緩存高可靠集群。
如果 Redis 緩存的主節(jié)點故障宕機,從節(jié)點可以切換成為主節(jié)點,繼續(xù)提供緩存服務,避免了由于 Redis 故障宕機而導致的緩存雪崩問題。
緩存擊穿
我們的業(yè)務通常會有幾個數(shù)據(jù)會被頻繁地訪問,比如秒殺活動,這類被頻地訪問的數(shù)據(jù)被稱為熱點數(shù)據(jù)。
如果緩存中的某個熱點數(shù)據(jù)過期了,此時大量的請求訪問了該熱點數(shù)據(jù),就無法從緩存中讀取,直接訪問數(shù)據(jù)庫,數(shù)據(jù)庫很容易就被高并發(fā)的請求沖垮,這就是緩存擊穿的問題。
可以發(fā)現(xiàn)緩存擊穿跟緩存雪崩很相似,你可以認為緩存擊穿是緩存雪崩的一個子集。
應對緩存擊穿可以采取前面說到兩種方案:
-
互斥鎖方案,保證同一時間只有一個業(yè)務線程更新緩存,未能獲取互斥鎖的請求,要么等待鎖釋放后重新讀取緩存,要么就返回空值或者默認值。
-
不給熱點數(shù)據(jù)設置過期時間,由后臺異步更新緩存,或者在熱點數(shù)據(jù)準備要過期前,提前通知后臺線程更新緩存以及重新設置過期時間;
緩存穿透
當發(fā)生緩存雪崩或擊穿時,數(shù)據(jù)庫中還是保存了應用要訪問的數(shù)據(jù),一旦緩存恢復相對應的數(shù)據(jù),就可以減輕數(shù)據(jù)庫的壓力,而緩存穿透就不一樣了。
當用戶訪問的數(shù)據(jù),既不在緩存中,也不在數(shù)據(jù)庫中,導致請求在訪問緩存時,發(fā)現(xiàn)緩存缺失,再去訪問數(shù)據(jù)庫時,發(fā)現(xiàn)數(shù)據(jù)庫中也沒有要訪問的數(shù)據(jù),沒辦法構(gòu)建緩存數(shù)據(jù),來服務后續(xù)的請求。那么當有大量這樣的請求到來時,數(shù)據(jù)庫的壓力驟增,這就是緩存穿透的問題。
緩存穿透的發(fā)生一般有這兩種情況:
-
業(yè)務誤操作,緩存中的數(shù)據(jù)和數(shù)據(jù)庫中的數(shù)據(jù)都被誤刪除了,所以導致緩存和數(shù)據(jù)庫中都沒有數(shù)據(jù);
-
黑客惡意攻擊,故意大量訪問某些讀取不存在數(shù)據(jù)的業(yè)務;
應對緩存穿透的方案,常見的方案有三種。
-
第一種方案,非法請求的限制;
-
第二種方案,緩存空值或者默認值;
-
第三種方案,使用布隆過濾器快速判斷數(shù)據(jù)是否存在,避免通過查詢數(shù)據(jù)庫來判斷數(shù)據(jù)是否存在;
第一種方案,非法請求的限制
當有大量惡意請求訪問不存在的數(shù)據(jù)的時候,也會發(fā)生緩存穿透,因此在 API 入口處我們要判斷求請求參數(shù)是否合理,請求參數(shù)是否含有非法值、請求字段是否存在,如果判斷出是惡意請求就直接返回錯誤,避免進一步訪問緩存和數(shù)據(jù)庫。
第二種方案,緩存空值或者默認值
當我們線上業(yè)務發(fā)現(xiàn)緩存穿透的現(xiàn)象時,可以針對查詢的數(shù)據(jù),在緩存中設置一個空值或者默認值,這樣后續(xù)請求就可以從緩存中讀取到空值或者默認值,返回給應用,而不會繼續(xù)查詢數(shù)據(jù)庫。
第三種方案,使用布隆過濾器快速判斷數(shù)據(jù)是否存在,避免通過查詢數(shù)據(jù)庫來判斷數(shù)據(jù)是否存在。
我們可以在寫入數(shù)據(jù)庫數(shù)據(jù)時,使用布隆過濾器做個標記,然后在用戶請求到來時,業(yè)務線程確認緩存失效后,可以通過查詢布隆過濾器快速判斷數(shù)據(jù)是否存在,如果不存在,就不用通過查詢數(shù)據(jù)庫來判斷數(shù)據(jù)是否存在。
即使發(fā)生了緩存穿透,大量請求只會查詢 Redis 和布隆過濾器,而不會查詢數(shù)據(jù)庫,保證了數(shù)據(jù)庫能正常運行,Redis 自身也是支持布隆過濾器的。
那問題來了,布隆過濾器是如何工作的呢?接下來,我介紹下。
布隆過濾器由「初始值都為 0 的位圖數(shù)組」和「 N 個哈希函數(shù)」兩部分組成。當我們在寫入數(shù)據(jù)庫數(shù)據(jù)時,在布隆過濾器里做個標記,這樣下次查詢數(shù)據(jù)是否在數(shù)據(jù)庫時,只需要查詢布隆過濾器,如果查詢到數(shù)據(jù)沒有被標記,說明不在數(shù)據(jù)庫中。
布隆過濾器會通過 3 個操作完成標記:
-
第一步,使用 N 個哈希函數(shù)分別對數(shù)據(jù)做哈希計算,得到 N 個哈希值;
-
第二步,將第一步得到的 N 個哈希值對位圖數(shù)組的長度取模,得到每個哈希值在位圖數(shù)組的對應位置。
-
第三步,將每個哈希值在位圖數(shù)組的對應位置的值設置為 1;
舉個例子,假設有一個位圖數(shù)組長度為 8,哈希函數(shù) 3 個的布隆過濾器。
在數(shù)據(jù)庫寫入數(shù)據(jù) x 后,把數(shù)據(jù) x 標記在布隆過濾器時,數(shù)據(jù) x 會被 3 個哈希函數(shù)分別計算出 3 個哈希值,然后在對這 3 個哈希值對 8 取模,假設取模的結(jié)果為 1、4、6,然后把位圖數(shù)組的第 1、4、6 位置的值設置為 1。當應用要查詢數(shù)據(jù) x 是否數(shù)據(jù)庫時,通過布隆過濾器只要查到位圖數(shù)組的第 1、4、6 位置的值是否全為 1,只要有一個為 0,就認為數(shù)據(jù) x 不在數(shù)據(jù)庫中。
布隆過濾器由于是基于哈希函數(shù)實現(xiàn)查找的,高效查找的同時存在哈希沖突的可能性,比如數(shù)據(jù) x 和數(shù)據(jù) y 可能都落在第 1、4、6 位置,而事實上,可能數(shù)據(jù)庫中并不存在數(shù)據(jù) y,存在誤判的情況。
所以,查詢布隆過濾器說數(shù)據(jù)存在,并不一定證明數(shù)據(jù)庫中存在這個數(shù)據(jù),但是查詢到數(shù)據(jù)不存在,數(shù)據(jù)庫中一定就不存在這個數(shù)據(jù)。
總結(jié)
緩存異常會面臨的三個問題:緩存雪崩、擊穿和穿透。
其中,緩存雪崩和緩存擊穿主要原因是數(shù)據(jù)不在緩存中,而導致大量請求訪問了數(shù)據(jù)庫,數(shù)據(jù)庫壓力驟增,容易引發(fā)一系列連鎖反應,導致系統(tǒng)奔潰。不過,一旦數(shù)據(jù)被重新加載回緩存,應用又可以從緩存快速讀取數(shù)據(jù),不再繼續(xù)訪問數(shù)據(jù)庫,數(shù)據(jù)庫的壓力也會瞬間降下來。因此,緩存雪崩和緩存擊穿應對的方案比較類似。
而緩存穿透主要原因是數(shù)據(jù)既不在緩存也不在數(shù)據(jù)庫中。因此,緩存穿透與緩存雪崩、擊穿應對的方案不太一樣。
我這里整理了表格,你可以從下面這張表格很好的知道緩存雪崩、擊穿和穿透的區(qū)別以及應對方案。
免責聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!