一個 Redis 的雪崩和穿透問題,小學(xué)妹畫了個圖,結(jié)果入職了
本文轉(zhuǎn)載自微信公眾號「Java極客技術(shù)」,作者鴨血粉絲。轉(zhuǎn)載本文請聯(lián)系Java極客技術(shù)公眾號。
阿粉的一個小學(xué)妹最近剛從某個小互聯(lián)網(wǎng)公司跳槽,然后最近面試的挺多的,一個不善言語的小姑娘,技術(shù)還是 OK 的,本來之前是做 UI 的,但是時間長了,感覺沒太大意思,所以就開始學(xué)了后端,然后從原有公司慢慢的轉(zhuǎn)為了后端開發(fā)人,也就是我們所說的 “程序猿”,最近面試給阿粉談了談她的面試經(jīng)驗。阿粉比較印象深刻的一句話就是,我給你畫個圖,你看一下,這是對面試官說的,事情是什么樣子的呢?
你了解 Redis 穿透和雪崩么?
為什么這么說,因為面試官當(dāng)你說到 Redis 的時候,面試官問的現(xiàn)在已經(jīng)不是 "你說一下 Redis 的幾種數(shù)據(jù)結(jié)構(gòu)" ,現(xiàn)在面試問的時候,很多都是對 Redis 的實際使用開始問了,比如說,
- Redis 都有哪些架構(gòu)模式?單機(jī)版,主從復(fù)制,哨兵機(jī)制,集群(proxy 型),集群(直連型)
- 使用過 Redis 分布式鎖么,它是怎么實現(xiàn)的?
- 使用過 Redis 做異步隊列么,你是怎么用的?有什么缺點?
- 什么是緩存穿透?如何避免?什么是緩存雪崩?何如避免?
而阿粉的小學(xué)妹遇到的就是關(guān)于 Redis 的緩存穿透和雪崩問題了。這個問題學(xué)妹配合了一波自己的 UI 功底圖加上口頭的解釋,于是成功的拿到了這個 Offer,也可能是因為小學(xué)妹比較美麗并且技術(shù)還過的去。所以,就準(zhǔn)備入職了。
我們來看看小學(xué)妹到底畫了什么圖,讓面試官問了一波之后就入職了。
緩存穿透
如圖:圖是阿粉找小學(xué)妹專門畫出來的,大家看一下
既然我們看完圖了,相信大家也都看到了什么是緩存穿透了,也就是說,在我們的緩存系統(tǒng)中,也就是 Redis 中,我們都是拿著我們的 Key 去 Redis 中去尋找 Value 中,如果說我們在 Redis 中找不到我們的數(shù)據(jù)之后,我們就會去數(shù)據(jù)庫中去尋找我們的數(shù)據(jù),如果只是單一請求的話,也不能算是個太大的問題,只能稱之為擊穿而已,但是如果說要是請求并發(fā)量很大的話,就會對我們的數(shù)據(jù)庫造成很大的壓力,這其實就稱之為緩存穿透,而穿透出現(xiàn)的嚴(yán)重后果,就會是緩存的雪崩了,我們先說穿透,一會再說雪崩。
那么都會有什么情況會造成緩存被穿透呢?
- 自身代碼問題
- 一些惡意攻擊、爬蟲造成大量空的命中。
如果有個黑客對你們公司的項目和數(shù)據(jù)庫比較感興趣,他就可能會給你整出巨多的一些不存在的ID,然后就瘋狂的去調(diào)用你們的某項接口,這些本身不存在的 ID 去查詢緩存的數(shù)據(jù)的時候,那就是壓根沒有的,這時候就會有大量的請求去訪問數(shù)據(jù)庫,雖然可能數(shù)據(jù)能支撐一段時間,但是早晚會讓人家給你整的涼了。
那么應(yīng)該怎么去解決緩存穿透的問題呢?
- 利用互斥鎖,緩存失效的時候,先去獲得鎖,得到鎖了,再去請求數(shù)據(jù)庫。沒得到鎖,則休眠一段時間重試。
- 采用異步更新策略,無論 Key 是否取到值,都直接返回。Value 值中維護(hù)一個緩存失效時間,緩存如果過期,異步起一個線程去讀數(shù)據(jù)庫,更新緩存。需要做緩存預(yù)熱(項目啟動前,先加載緩存)操作。
- 提供一個能迅速判斷請求是否有效的攔截機(jī)制,比如,利用布隆過濾器,內(nèi)部維護(hù)一系列合法有效的 Key。迅速判斷出,請求所攜帶的 Key 是否合法有效。如果不合法,則直接返回。
- 布隆過濾器實際上是一種比較推薦的方式。
布隆過濾器的實現(xiàn)原理則是這樣的:
當(dāng)一個變量被加入集合時,通過 K 個映射函數(shù)將這個變量映射成位圖中的 K 個點,把它們置為 1。查詢某個變量的時候我們只要看看這些點是不是都是 1 就可以大概率知道集合中有沒有它了,如果這些點有任何一個 0,則被查詢變量一定不在;如果都是 1,則被查詢變量很可能在。注意,這里是可能存在,不一定一定存在!這就是布隆過濾器的基本思想。
而當(dāng)你說出布隆過濾器的時候,可能這才是面試官想要問你的內(nèi)容,這時候你就得好好的和面試官開始聊聊什么事布隆過濾器了。
我們還是繼續(xù)用大眾都想看到的圖解來解釋布隆過濾器。
字符串 "Java" 在經(jīng)過四個映射函數(shù)操作后在位圖上有四個點被設(shè)置成了 1。當(dāng)我們需要判斷 “ziyou” 字符串是否存在的時候只要在一次對字符串進(jìn)行映射函數(shù)的操作,得到四個 1 就說明 “Java” 是可能存在的。
注意語言,是可能存在,而不是一定存在,
那是因為映射函數(shù)本身就是散列函數(shù),散列函數(shù)是會有碰撞的,意思也就是說會存在一個字符串可能是 “Java1” 經(jīng)過相同的四個映射函數(shù)運(yùn)算得到的四個點跟 “Java” 可能是一樣的,這種情況下我們就說出現(xiàn)了誤算。
另外還有可能這四個點位上的 1 是四個不同的變量經(jīng)過運(yùn)算后得到的,這也不能證明字符串 “Java” 是一定存在的。
而我們使用布隆過濾器其實就是提供一個能迅速判斷請求是否有效的攔截機(jī)制,判斷出請求所攜帶的 Key 是否合法有效。如果不合法,則直接返回。
而阿粉的小學(xué)妹給面試官解釋了一波這操作之后,看樣子,面試官對這個“程序猿”開始有點印象了,接下來就順著問了,那什么事緩存的雪崩呢?
緩存雪崩
這時候也就是說,當(dāng)我們有多個請求訪問緩存的時候,這時候,緩存中的數(shù)據(jù)是沒有的,也就是說緩存同一時間大面積的失效,這個時候又來了一波請求,結(jié)果請求都懟到數(shù)據(jù)庫上,從而導(dǎo)致數(shù)據(jù)庫連接異常
他和穿透實際上相似但是又有所不同,相似的地方是都是搞數(shù)據(jù)庫,不同的是緩存穿透是指并發(fā)查同一條數(shù)據(jù),緩存雪崩是不同數(shù)據(jù)都過期了,很多數(shù)據(jù)都查不到從而查數(shù)據(jù)庫
而解決緩存雪崩的策略也是比較多的,而且都是比較實用的。比如:
- 給緩存的失效時間,加上一個隨機(jī)值,避免集體失效。
- 雙緩存。我們有兩個緩存,緩存 A 和緩存 B。緩存 A 的失效時間為 20 分鐘,緩存 B 不設(shè)失效時間
雙緩存策略比較有意思,當(dāng)請求來臨的時候,我們先從 A 緩存中獲取,如果 A 緩存有數(shù)據(jù),那么直接給他返回,如果 A 中沒有數(shù)據(jù),那么就直接從 B 中獲取數(shù)據(jù),直接返回,與此同時,我們啟動一個更新的線程,更新 A 緩存和 B 緩存,這就是雙緩存的策略。
上述的處理緩存雪崩的情況實際上都是從代碼上來進(jìn)行實現(xiàn),而我們換個思路考慮呢,也就是從架構(gòu)的方向去考慮的話,解決方案就是以下的幾種了。
- 限流
- 降級
- 熔斷
那么怎么實現(xiàn)限流呢?
說到限流降級了,那就不能單純的去針對 Redis 出現(xiàn)的問題而進(jìn)行處理了,而實際上是為了保證用戶保護(hù)服務(wù)的穩(wěn)定性來進(jìn)行的。
那么為什么要去限流呢?你要單純的說是為了保證系統(tǒng)的穩(wěn)定性,那面試官估計得崩潰,這和沒說有啥區(qū)別,你得舉個簡單的例子才能正兒八經(jīng)的忽悠住面試官,比如:
假設(shè),我們當(dāng)前的程序能夠處理10個請求,結(jié)果第二天,忽然有200多請求一起過來,整整翻了20倍,這時候,程序就涼了,但是如果第一天晚上的時候,領(lǐng)導(dǎo)給你說,明天你寫的那個程序大約會有200多個請求要處理,你這時候是不是得想辦法,比如說,能不能再寫出另外的一段程序來進(jìn)行分擔(dān)請求,這時候其實就相當(dāng)于需要我們?nèi)ハ蘖髁恕?/p>
限流算法之漏桶算法
同樣的,我們整個圖來理解一下這個算法到底是怎么實現(xiàn)的。
如果一桶有一個細(xì)眼,我們往里面裝水,可以看到水是一滴一滴勻速的下落的,如果桶滿了就拒絕水滴繼續(xù)滴入,沒滿的話就繼續(xù)裝水,實際上就是這樣的水滴實際上就相當(dāng)于是請求,如果水桶沒滿的時候,還能繼續(xù)處理我們進(jìn)來的請求,當(dāng)水桶滿了的時候,就拒絕處理,讓他溢出。
前提是我們的這個桶是個固定的容器,不能隨著水的增多桶會變大,要不然那還用什么限流算法。
簡單的漏桶算法的實現(xiàn):
- public class LeakyBucket {
- public long timeStamp = System.currentTimeMillis(); // 當(dāng)前時間
- public long capacity; // 桶的容量
- public long rate; // 水漏出的速度
- public long water; // 當(dāng)前水量(當(dāng)前累積請求數(shù))
- public boolean grant() {
- long now = System.currentTimeMillis();
- // 先執(zhí)行漏水,計算剩余水量
- water = Math.max(0, water - (now - timeStamp) * rate);
- timeStamp = now;
- if ((water + 1) < capacity) {
- // 嘗試加水,并且水還未滿
- water += 1;
- return true;
- } else {
- // 水滿,拒絕加水
- return false;
- }
- }
- }
上面的代碼是來自悟空,不得不說,這個簡單的例子雖然簡單,但是把這個漏桶算法的簡單原理描述的還是差不多的,而在這里最需要注意的,就是桶的容量,還有就是水桶漏洞的出水的速度。
既然我們了解了漏桶算法是如何實現(xiàn)限流的,那么必然也會有他處理不來的情況,因為我們已經(jīng)定義了水漏出的速度,而這時候如果應(yīng)對突發(fā)的流量忽然涌進(jìn)來,他處理起來效率就不夠高了,因為水桶滿了之后,請求都拒絕了,都不處理了。
其實我們所說的漏桶算法還可以看作是一個帶有常量服務(wù)時間的單服務(wù)器隊列,如果漏桶(包緩存)溢出,那么數(shù)據(jù)包會被丟棄。
而我們的漏桶算法主要是能夠強(qiáng)行限制數(shù)據(jù)的傳輸速率。
那么又有什么算法能夠不進(jìn)行強(qiáng)制限制傳輸速率,并且實現(xiàn)限流呢?
令牌桶算法
我們感謝百度,我從百度圖片中找了個一個比較給力的圖來描述令牌桶的算法。
令牌桶算法的基本過程是這個樣子的:
- 用戶配置的平均發(fā)送速率為r,則每隔1/r秒一個令牌被加入到桶中
- 假設(shè)桶最多可以存發(fā)b個令牌。如果令牌到達(dá)時令牌桶已經(jīng)滿了,那么這個令牌會被丟棄
- 當(dāng)一個n個字節(jié)的數(shù)據(jù)包到達(dá)時,就從令牌桶中刪除n個令牌,并且數(shù)據(jù)包被發(fā)送到網(wǎng)絡(luò)
- 如果令牌桶中少于n個令牌,那么不會刪除令牌,并且認(rèn)為這個數(shù)據(jù)包在流量限制之外
乍一看,怎么感覺這個令牌桶和漏桶這么像,一個是水滴,一個是令牌,實際上不是。
令牌桶這種控制機(jī)制基于令牌桶中是否存在令牌來指示什么時候可以發(fā)送流量。令牌桶中的每一個令牌都代表一個字節(jié)。如果令牌桶中存在令牌,則允許發(fā)送流量;而如果令牌桶中不存在令牌,則不允許發(fā)送流量。
而且他是能夠應(yīng)對突發(fā)限制的,雖然傳輸?shù)乃俾适艿搅讼拗?所以它適合于具有突發(fā)特性的流量的一種算法。
而在 Google 開源工具包中的限流工具類RateLimiter ,這個類就是根據(jù)令牌桶算法來完成限流。大家有興趣的可以去看看呀。
漏桶算法和令牌桶算法的區(qū)別
漏桶算法與令牌桶算法實際上看起來有點相似,但是不能混淆哈,這就是阿粉在上面說的:
漏桶算法能夠強(qiáng)行限制數(shù)據(jù)的傳輸速率。
令牌桶算法能夠在限制數(shù)據(jù)的平均傳輸速率的同時還允許某種程度的突發(fā)傳輸