Redis 緩存雪崩、緩存穿透、緩存擊穿、緩存降級(jí)、緩存預(yù)熱、緩存更新
Redis緩存能夠有效地加速應(yīng)用的讀寫速度,就DB來(lái)說(shuō),Redis成績(jī)已經(jīng)很驚人了,且不說(shuō)memcachedb和Tokyo Cabinet之流,就說(shuō)原版的memcached,速度似乎也只能達(dá)到這個(gè)級(jí)別。今天主要講講在使用Redis時(shí)經(jīng)常遇到的幾個(gè)問(wèn)題。緩存雪崩、緩存擊穿、緩存穿透、緩存預(yù)熱、緩存更新、緩存降級(jí)。
v緩存雪崩
緩存雪崩,是指在某一個(gè)時(shí)間段,緩存集中過(guò)期失效。所有原本應(yīng)該訪問(wèn)緩存的請(qǐng)求都去查詢數(shù)據(jù)庫(kù)了,而對(duì)數(shù)據(jù)庫(kù)CPU和內(nèi)存造成巨大壓力,嚴(yán)重的會(huì)造成數(shù)據(jù)庫(kù)宕機(jī)。從而形成一系列連鎖反應(yīng),造成整個(gè)系統(tǒng)崩潰。
緩存雪崩示意圖:

緩存失效時(shí)的雪崩效應(yīng)對(duì)底層系統(tǒng)的沖擊非常致命,那么應(yīng)對(duì)Redis緩存雪崩有哪些方案呢?
1.1 加鎖或者隊(duì)列
可以考慮用加鎖或者隊(duì)列的方式防止大量線程對(duì)數(shù)據(jù)庫(kù)的一次性進(jìn)行讀寫,避免緩存失效時(shí)對(duì)數(shù)據(jù)庫(kù)造成的巨大沖擊。

以上效果還可以考慮接入Redis鎖實(shí)現(xiàn),具體可以參考《SpringBoot進(jìn)階教程(二十七)整合Redis之分布式鎖》?加鎖或者隊(duì)列都是一個(gè)非常淺顯的辦法。雖然能夠在一定的程度上緩解了數(shù)據(jù)庫(kù)的壓力,但同時(shí)也極大的降低了系統(tǒng)的吞吐量。
1.2 協(xié)調(diào)Redis過(guò)期時(shí)間
分析用戶行為,盡量讓緩存失效的時(shí)間均勻分布,最次也得隨機(jī)分布,尤其是一些訪問(wèn)大的接口
@Override public UserDetails getUserInfoById(Integer uid){ String key = String.format("user_info_id:%d",uid); UserDetails userDetails = (UserDetails)templateRedis.opsForValue().get(key); if(userDetails != null){ return userDetails; }else{ userDetails = userDetailsMapper.getUserDetailsByUid(uid); Random random = new Random(); int time = 600; // type: 1: 大V用戶 2: 網(wǎng)紅 3: 普通用戶 if(userDetails != null){ if(userDetails.getType() == 1){ time = 3600 + random.nextInt(3600); // 如果有其他邏輯 }else if(userDetails.getType() == 2){ time = 1200 + random.nextInt(1200); // 如果有其他邏輯 }else{ // 如果有其他邏輯 } redisTemplate.opsForValue().set(key, userDetails, time, TimeUnit.SECONDS); } } return userDetails; }
這里主要還是結(jié)合業(yè)務(wù)場(chǎng)景讓緩存失效的時(shí)間均勻分布,比如上面這段代碼中,大V用戶和網(wǎng)紅用戶一般粉絲都是上百萬(wàn),所以可以緩存長(zhǎng)點(diǎn)的時(shí)間也是可以的。
1.3 二級(jí)緩存
做二級(jí)緩存,A1為原始緩存,A2為拷貝緩存,A1失效時(shí),可以訪問(wèn)A2,A1緩存失效時(shí)間設(shè)置為短期,A2設(shè)置為長(zhǎng)期。
1.4 保證緩存層服務(wù)高可用性
保證緩存層服務(wù)高可用性。如果緩存層設(shè)計(jì)成高可用的,即使個(gè)別節(jié)點(diǎn)、個(gè)別機(jī)器、甚至是機(jī)房宕掉,依然可以提供服務(wù)。
關(guān)于這一塊,可以看看前面寫到的《SpringBoot進(jìn)階教程(三十)整合Redis之Sentinel哨兵模式》和《詳解Redis Cluster集群》。
1.5 依賴隔離組件為后端限流并降級(jí)。
需要對(duì)重要的資源(例如Redis、MySQL、外部接口)都進(jìn)行隔離,讓每種資源都單獨(dú)運(yùn)行在自己的線程池中。即使個(gè)別資源出現(xiàn)了問(wèn)題,對(duì)其他服務(wù)沒(méi)有影響。但是線程池如何管理,比如如何關(guān)閉資源池、開(kāi)啟資源池、資源池閥值管理,這些做起來(lái)還是相當(dāng)復(fù)雜的。
v緩存穿透
緩存穿透是指查詢一個(gè)一定不存在的數(shù)據(jù)。由于緩存不命中,并且出于容錯(cuò)考慮,如果從數(shù)據(jù)庫(kù)查不到數(shù)據(jù)則不寫入緩存,這將導(dǎo)致這個(gè)不存在的數(shù)據(jù)每次請(qǐng)求都要到數(shù)據(jù)庫(kù)去查詢,失去了緩存的意義。
緩存穿透示意圖:

如何解決緩存穿透?對(duì)應(yīng)的幾個(gè)參考方案:
2.1 布隆過(guò)濾器(BloomFilter)
采用布隆過(guò)濾器,將所有可能存在的數(shù)據(jù)哈希到一個(gè)足夠大的bitmap中,一個(gè)一定不存在的數(shù)據(jù)會(huì)被這個(gè)bitmap攔截掉,從而避免了對(duì)底層存儲(chǔ)系統(tǒng)的查詢壓力。由于請(qǐng)求的參數(shù)是不合法的(每次都請(qǐng)求不存在的參數(shù)),于是我們可以使用布隆過(guò)濾器(BloomFilter)或者壓縮filter提前攔截,不合法就不讓這個(gè)請(qǐng)求到數(shù)據(jù)庫(kù)層。
BloomFilter就類似于一個(gè)hash set,用于快速判某個(gè)元素是否存在于集合中,其典型的應(yīng)用場(chǎng)景就是快速判斷一個(gè)key是否存在于某容器,不存在就直接返回。布隆過(guò)濾器的關(guān)鍵就在于hash算法和容器大小,
2.2 將空對(duì)象記錄在緩存中。
如果數(shù)據(jù)庫(kù)返回信息為null,也可以將這個(gè)空對(duì)象設(shè)置到緩存里邊去。下次再請(qǐng)求的時(shí)候,就可以從緩存里邊獲取了,將空對(duì)象設(shè)置一個(gè)較短的過(guò)期時(shí)間。如1.1中的代碼示例所示。
v緩存擊穿
緩存擊穿指的是熱點(diǎn)key在某個(gè)特殊的場(chǎng)景時(shí)間內(nèi)恰好失效了,恰好有大量并發(fā)請(qǐng)求過(guò)來(lái)了,造成DB壓力。
其實(shí)緩存擊穿和緩存雪崩從概念上來(lái)講差不多,只是緩存擊穿是某些熱點(diǎn)key,而雪崩指的是大規(guī)模的key。
如何解決緩存擊穿,對(duì)應(yīng)的幾個(gè)參考方案:
3.1 與1.1中類似,通過(guò)加鎖或者隊(duì)列的方式防止大量請(qǐng)求透過(guò)redis到DB中。
3.2 對(duì)于一些熱點(diǎn)key,過(guò)期時(shí)間可以無(wú)限調(diào)長(zhǎng)
將熱點(diǎn)key過(guò)期時(shí)間無(wú)限調(diào)長(zhǎng),然后通過(guò)job服務(wù)來(lái)管理這些熱點(diǎn)key不會(huì)過(guò)期,保證熱點(diǎn)key(尤其是像排行榜、首頁(yè)熱度等需要大量計(jì)算的熱點(diǎn)key)的穩(wěn)定性。需要注意的是,job服務(wù)本身也存在不穩(wěn)定性,比如部署job的服務(wù)掛了之類的。這里可以看看之前的一篇文章。《詳解Supervisor進(jìn)程守護(hù)監(jiān)控》
v緩存降級(jí)
緩存降級(jí)是指當(dāng)訪問(wèn)量劇增、服務(wù)出現(xiàn)問(wèn)題(如響應(yīng)時(shí)間慢或不響應(yīng))或非核心服務(wù)影響到核心流程的性能時(shí),仍然需要保證服務(wù)還是可用的,即使是有損服務(wù)。系統(tǒng)可以根據(jù)一些關(guān)鍵數(shù)據(jù)進(jìn)行自動(dòng)降級(jí),也可以配置開(kāi)關(guān)實(shí)現(xiàn)人工降級(jí)。
降級(jí)的最終目的是保證核心服務(wù)可用,即使是有損的。而且有些服務(wù)是無(wú)法降級(jí)的(如加入購(gòu)物車、結(jié)算)。

參加過(guò)去年天貓雙11的朋友應(yīng)該很清楚的能感受到降級(jí),當(dāng)時(shí)被吐槽的最狠的降級(jí)應(yīng)該就是加入購(gòu)物車,在結(jié)算的時(shí)候無(wú)法更改收貨地址。只能使用默認(rèn)收貨地址,這得成就多少"前男友前女友"啊。
在進(jìn)行降級(jí)之前要對(duì)系統(tǒng)進(jìn)行梳理,看看系統(tǒng)是不是可以丟卒保帥;從而梳理出哪些必須誓死保護(hù),哪些可降級(jí);比如可以參考日志級(jí)別設(shè)置預(yù)案:
一般:比如有些服務(wù)偶爾因?yàn)榫W(wǎng)絡(luò)抖動(dòng)或者服務(wù)正在上線而超時(shí),可以自動(dòng)降級(jí);
警告:有些服務(wù)在一段時(shí)間內(nèi)成功率有波動(dòng)(如在95~100%之間),可以自動(dòng)降級(jí)或人工降級(jí),并發(fā)送告警;
錯(cuò)誤:比如可用率低于90%,或者數(shù)據(jù)庫(kù)連接池被打爆了,或者訪問(wèn)量突然猛增到系統(tǒng)能承受的最大閥值,此時(shí)可以根據(jù)情況自動(dòng)降級(jí)或者人工降級(jí);
嚴(yán)重錯(cuò)誤:比如因?yàn)樘厥庠驍?shù)據(jù)錯(cuò)誤了,此時(shí)需要緊急人工降級(jí)。
ps:強(qiáng)于"馬爸爸",在雙11的海量并發(fā)面前,也得降級(jí),無(wú)可厚非。只是大家在做降級(jí)的時(shí)候,一定得考慮好取舍。這反而是降級(jí)最大的難度。
v緩存預(yù)熱
上初中第一次做化學(xué)實(shí)驗(yàn)的時(shí)候,大家就知道試管加熱前需要先預(yù)熱。緩存預(yù)熱也是一個(gè)比較常見(jiàn)的概念,緩存預(yù)熱就是系統(tǒng)上線后,將相關(guān)的緩存數(shù)據(jù)直接加載到緩存系統(tǒng)。這樣就可以避免在用戶請(qǐng)求的時(shí)候,先查詢數(shù)據(jù)庫(kù),然后再將數(shù)據(jù)緩存的問(wèn)題。用戶直接查詢事先被預(yù)熱的緩存數(shù)據(jù)。
緩存預(yù)熱思路:
直接寫個(gè)緩存刷新頁(yè)面,上線時(shí)手工操作下;
數(shù)據(jù)量不大,可以在項(xiàng)目啟動(dòng)的時(shí)候自動(dòng)進(jìn)行加載;
定時(shí)刷新緩存;
對(duì)于一些計(jì)算量非常大的接口,緩存預(yù)熱肯定是得有的。
v緩存更新
除了緩存服務(wù)器自帶的緩存失效策略之外(Redis默認(rèn)的有6中策略可供選擇),我們還可以根據(jù)具體的業(yè)務(wù)需求進(jìn)行自定義的緩存淘汰,常見(jiàn)的策略有兩種:
定時(shí)任務(wù)去清理過(guò)期的緩存;
提供手動(dòng)清理緩存的接口。
當(dāng)有用戶請(qǐng)求過(guò)來(lái)時(shí),再判斷這個(gè)請(qǐng)求所用到的緩存是否過(guò)期,過(guò)期的話就去底層系統(tǒng)得到新數(shù)據(jù)并更新緩存。
上面的這幾個(gè)方案各有優(yōu)劣,第一種的缺點(diǎn)是維護(hù)大量緩存的key是比較麻煩的;第二種人工成本太高;第三種的缺點(diǎn)就是每次用戶請(qǐng)求過(guò)來(lái)都要判斷緩存失效,邏輯相對(duì)比較復(fù)雜。具體使用場(chǎng)景還得結(jié)合業(yè)務(wù)來(lái)區(qū)分對(duì)待。
v博客總結(jié)
Redis的出現(xiàn)確實(shí)很大程度上解決了sql的壓力,善用Redis的各種機(jī)制已經(jīng)成為一個(gè)必不可少的技能之一。
v源碼地址
https://github.com/toutouge/javademo/tree/master/hellospringboot
作者:請(qǐng)叫我頭頭哥?
出處:http://www.cnblogs.com/toutou/?
原文鏈接:https://www.cnblogs.com/toutou/p/redis_question.html
本文轉(zhuǎn)自博客園網(wǎng),版權(quán)歸原作者所有。
