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