使用spring-cache一行代碼解決緩存擊穿問題
引言
今天,重新回顧一下緩存擊穿這個(gè)問題! 之所以寫這個(gè)文章呢,因?yàn)槟壳熬W(wǎng)上流傳的文章落地性太差(什么布隆過濾器啊,布谷過濾器啊,嗯,你們懂的),其實(shí)這類方案并不適合在項(xiàng)目中直接落地。
那么,我們?cè)陧?xiàng)目中落地代碼的時(shí)候,其實(shí)只需要一個(gè)注解就能解決這些問題,并不需要搞的那么復(fù)雜。
本文有一個(gè)前提,讀者必須是java棧,且是用Springboot構(gòu)建自己的項(xiàng)目,如果是go技術(shù)?;蛘遬ython技術(shù)棧的,可能介紹的思路僅供大家參考!
正文
目前缺陷
首先,為什么說目前網(wǎng)上流傳的方案,落地性差呢,因?yàn)槎既狈σ粋€(gè)可以和SpringBoot結(jié)合起來的真實(shí)場(chǎng)景,基本上都脫離了SpringBoot,只站在Java這個(gè)層級(jí)去分析。那問題就來了,現(xiàn)在還有只用SpringMvc,卻不用SpringBoot的公司么?因此,本文嘗試將該方案和SpringBoot結(jié)合起來,講一個(gè)確實(shí)可行,可以落地的方案!
當(dāng)然,我們先來說說目前在網(wǎng)上流傳的幾套方案,到底不靠譜在哪里!
(1)布隆過濾器
關(guān)于布隆過濾器,我就不介紹太多,這里就理解為是一個(gè)過濾器,用于快速檢索一個(gè)元素是否在一個(gè)集合中;那么當(dāng)一個(gè)請(qǐng)求來的時(shí)候,快速判斷這個(gè)請(qǐng)求的key是否在指定集合中!如果在,說明有效,則放行。如果不在,則無效攔截。 至于實(shí)現(xiàn),各大博客也說了用了google提供的
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>19.0</version> </dependency>
這個(gè)包里有現(xiàn)成寫好的java類給你使用了,當(dāng)然demo代碼我就不貼了,一抓一大把! 當(dāng)然,似乎看上去完美無暇!一切都是那么的合適!
然而到這里,我就真的問一句,你們真的用了這個(gè)方案了?
我如果猜的沒錯(cuò),應(yīng)該沒幾個(gè)人遇到過緩存擊穿問題~
更何況,證明這個(gè)說法的正確性~
該方案最大的一個(gè)問題是布隆過濾器不支持反向刪除操作,例如你的項(xiàng)目里活躍的key的數(shù)量只有1000w個(gè),但是全部key數(shù)量有5000w個(gè),那這5000w個(gè)key會(huì)全部存在布隆過濾器里!
直到某一天,你會(huì)發(fā)現(xiàn)這個(gè)過濾器太擁擠了,誤判率太高,不得不進(jìn)行重建!
so,你們覺得這個(gè)做法真的靠譜?
那么布隆過濾器這個(gè)說法出自哪里呢? (大家一定很好奇對(duì)不對(duì)!)
當(dāng)然是xx機(jī)構(gòu)~~此處保護(hù)自己的狗頭~~記住,他們?yōu)榱烁罹虏耍欢〞?huì)選擇一些看起來極為高端,但是落地巨不靠譜的方案(這也是區(qū)分一個(gè)機(jī)構(gòu)到底是割韭菜還是真正有水平的標(biāo)桿,小白不懂,很容易被坑)~~看到這里,真是慚愧,我的第一篇文章也是寫這個(gè)方案了,但是在落地過程中,發(fā)現(xiàn)了不對(duì)勁(此處省略一萬字的檢討文,煙哥垃圾~~)。
(2)布谷過濾器
那么,為了解決布隆過濾器查詢性能弱、空間利用效率低、不支持反向操作等問題,又有一篇文章誕生了,主張用布谷過濾器來解決緩存擊穿問題!
但是,神奇的事情來了,基本上所有的文章都在說布谷過濾器多么多么牛逼,卻沒有任何落地的方案~
記住,我們平時(shí)寫代碼,一定是怎么方便怎么來!再記住,面試是一回事,代碼落地是另一回事~
那,真正簡(jiǎn)便的方案是什么樣的呢?來,我們一步步來~
真正方案
假設(shè),你此刻用的是springboot-2.x的版本,你為了能夠連接redis,你在pom文件里加入如下依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
然后呢,我們修改application.yml
spring: datasource: ... redis: database: ... host: ... port: ... (省事,不全貼了)
ok,說到這里,就不得不說一下spring-cache了,Spring3.1之后,引入了注解緩存技術(shù),其本質(zhì)上不是一個(gè)具體的緩存實(shí)現(xiàn)方案,而是一個(gè)對(duì)緩存使用的抽象,通過在既有代碼中添加少量自定義的各種annotation,即能夠達(dá)到使用緩存對(duì)象和緩存方法的返回對(duì)象的效果。Spring的緩存技術(shù)具備相當(dāng)?shù)撵`活性,不僅能夠使用SpEL(Spring Expression Language)來定義緩存的key和各種condition,還提供開箱即用的緩存臨時(shí)存儲(chǔ)方案,也支持和主流的專業(yè)緩存集成。
例如:我們?cè)诖a中經(jīng)常有這么一段邏輯,在目標(biāo)方法執(zhí)行前,會(huì)根據(jù)key先去緩存中查詢看是否有數(shù)據(jù),有就直接返回緩存中的key對(duì)應(yīng)的value值,不再執(zhí)行目標(biāo)方法;沒有則執(zhí)行目標(biāo)方法,去數(shù)據(jù)庫(kù)查詢出對(duì)應(yīng)的value,并以鍵值對(duì)的形式存入緩存。
如果我們不使用例如spring-cache的注解框架,你的代碼中會(huì)充斥著大量冗余代碼,而用了該框架后,以@Cacheable注解為例, 該注解在方法上,表示該方法的返回結(jié)果是可以緩存的。
也就是說,該方法的返回結(jié)果會(huì)放在緩存中,以便于以后使用相同的參數(shù)調(diào)用該方法時(shí),會(huì)返回緩存中的值,而不會(huì)實(shí)際執(zhí)行該方法。
那么,你的代碼只需要這么寫
@Override @Cacheable("menu") public Menu findById(String id) { Menu menu = this.getById(id); if (menu != null){ System.out.println("menu.name = " + menu.getName()); } return menu; }
在這個(gè)例子中,findById 方法與一個(gè)名為 menu 的緩存關(guān)聯(lián)起來了。調(diào)用該方法時(shí),會(huì)檢查 menu 緩存,如果緩存中有結(jié)果,就不會(huì)去執(zhí)行方法了。
ok,說到這里,其實(shí)都是大家懂得東西?。〗酉聛黹_始我們的主題:如何解決緩存擊穿問題!順便講講穿透和雪崩問題!
來來來,我們回憶一下緩存擊穿,穿透以及緩存雪崩的概念!
緩存穿透
在高并發(fā)下,查詢一個(gè)不存在的值時(shí),緩存不會(huì)被命中,導(dǎo)致大量請(qǐng)求直接落到數(shù)據(jù)庫(kù)上,如活動(dòng)系統(tǒng)里面查詢一個(gè)不存在的活動(dòng)。 多嘴一句:緩存穿透是指,請(qǐng)求的是緩存和數(shù)據(jù)庫(kù)中都沒有的數(shù)據(jù)!
對(duì)于緩存穿透問題,有一個(gè)很簡(jiǎn)單的解決方案,就是緩存NULL值~從緩存取不到的數(shù)據(jù),在數(shù)據(jù)庫(kù)中也沒有取到,直接返回空值。
那么spring-cache中,有一個(gè)配置是這樣的
spring.cache.redis.cache-null-values=true
帶上該配置后,就可以緩存null值了,值得一提的是,這個(gè)緩存時(shí)間要設(shè)的少一點(diǎn),例如15秒就夠,如果設(shè)置過長(zhǎng),會(huì)導(dǎo)致正常的緩存也無法使用。
緩存擊穿
在高并發(fā)下,對(duì)一個(gè)特定的值進(jìn)行查詢,但是這個(gè)時(shí)候緩存正好過期了,緩存沒有命中,導(dǎo)致大量請(qǐng)求直接落到數(shù)據(jù)庫(kù)上,如活動(dòng)系統(tǒng)里面查詢活動(dòng)信息,但是在活動(dòng)進(jìn)行過程中活動(dòng)緩存突然過期了。 多嘴一句:緩存擊穿是指,請(qǐng)求的是緩存沒有,而數(shù)據(jù)庫(kù)中有的數(shù)據(jù)!
記住,解決擊穿的最簡(jiǎn)單的方法,只有一個(gè),就是限流!至于怎么限,其實(shí)可以各顯神通!例如其他文章提到的布隆過濾器,布谷過濾器等,不過是限流方式之一而已!甚至,你用一些其他的限流組件也是可以的!
這里就要說spring-cahce的另一個(gè)配置了!
在緩存過期之后,如果多個(gè)線程同時(shí)請(qǐng)求對(duì)某個(gè)數(shù)據(jù)的訪問,會(huì)同時(shí)去到數(shù)據(jù)庫(kù),導(dǎo)致數(shù)據(jù)庫(kù)瞬間負(fù)荷增高。Spring4.3為@Cacheable注解提供了一個(gè)新的參數(shù)“sync”(boolean類型,缺省為false),當(dāng)設(shè)置它為true時(shí),只有一個(gè)線程的請(qǐng)求會(huì)去到數(shù)據(jù)庫(kù),其他線程都會(huì)等待直到緩存可用。這個(gè)設(shè)置可以減少對(duì)數(shù)據(jù)庫(kù)的瞬間并發(fā)訪問。
看到這里??!這不就是一個(gè)限流方案么?
所以解決方法就是,加一個(gè)屬性sync=true,就行。代碼就像下面這樣
@Cacheable(cacheNames="menu", sync="true")
用了該屬性后,可以指示底層將緩存鎖住,使只有一個(gè)線程可以進(jìn)入計(jì)算,而其他線程堵塞,直到返回結(jié)果更新到緩存中。
當(dāng)然,看到這里,一定會(huì)有人和我抬杠!他的問題是這樣的!
你這個(gè)只是針對(duì)單機(jī)的限流,并不是整體集群的限流!也就是說,假設(shè)你的集群搭建了3000個(gè)pod,最差的情況下就是,3000個(gè)pod上,每個(gè)pod都會(huì)發(fā)起一個(gè)請(qǐng)求去數(shù)據(jù)庫(kù)查詢,照樣還是會(huì)導(dǎo)致數(shù)據(jù)庫(kù)連接數(shù)不夠用,等等資源問題!
對(duì)于這個(gè)問題我只能說!少年,但凡你的公司產(chǎn)品達(dá)到這種流量規(guī)模,此刻你就不會(huì)在看我的文章!你此刻關(guān)心的問題是:
(1)哎,買深圳灣一號(hào)還是深圳灣公館呢,糾結(jié)!
(2)昨天美股又跌了,又損失了兩套房
(3)昨天提前撤單了,又少掙了幾萬
....(省略一萬字)
當(dāng)然,如果你非要解決,也有辦法。spring的aop有套路的,比如@Transactional的Advice是TransactionInterceptor,那么cache也對(duì)應(yīng)對(duì)一個(gè)CacheInterceptor,我們只要去改CacheInterceptor,這個(gè)切面就能解決。在里頭做一個(gè)分布式鎖!偽代碼如下
flag := 取分布式鎖 if flag { 走數(shù)據(jù)庫(kù)查詢,并緩存結(jié)果 }{ 睡眠一段時(shí)間,再次嘗試獲取key的值 }
但是,我還是要多嘴提一句,真沒必要~~ 記住一句話,立足實(shí)際出發(fā)~但凡你的業(yè)務(wù)到了那種級(jí)別,是可以做到區(qū)域部署的,完全可以規(guī)避開這類問題。
緩存雪崩
在高并發(fā)下,大量的緩存key在同一時(shí)間失效,導(dǎo)致大量的請(qǐng)求落到數(shù)據(jù)庫(kù)上,如活動(dòng)系統(tǒng)里面同時(shí)進(jìn)行著非常多的活動(dòng),但是在某個(gè)時(shí)間點(diǎn)所有的活動(dòng)緩存全部過期。
那么針對(duì)該問題,最簡(jiǎn)單的解決方法就是,過期時(shí)間加隨機(jī)值!
但是很麻煩的是,我們?cè)谑褂聾Cacheable注解的時(shí)候,原生功能沒法直接設(shè)置隨機(jī)過期時(shí)間的。
這個(gè)老實(shí)說,真沒啥好方法,只能自己繼承RedisCache
,對(duì)其增強(qiáng),改寫其中的put方法,帶上隨機(jī)時(shí)間!
(本文不贅述,自己可以去查閱相關(guān)博客,我真的不喜歡寫文章貼大量代碼,可讀性太差了,知道這么個(gè)思路就行,出門搜索一下,一堆答案!)
文末
自此,緩存擊穿,穿透,雪崩問題都得到圓滿解決~~
到此這篇關(guān)于使用spring-cache一行代碼解決緩存擊穿問題的文章就介紹到這了,更多相關(guān)spring-cache緩存擊穿內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java數(shù)據(jù)結(jié)構(gòu)與算法之循環(huán)隊(duì)列的實(shí)現(xiàn)
循環(huán)隊(duì)列 (Circular Queue) 是一種特殊的隊(duì)列。循環(huán)隊(duì)列解決了隊(duì)列出隊(duì)時(shí)需要將所有數(shù)據(jù)前移一位的問題。本文將帶大家詳細(xì)了解循環(huán)隊(duì)列如何實(shí)現(xiàn),需要的朋友可以參考一下2021-12-12spring?boot入門之誕生背景及優(yōu)勢(shì)影響
這篇文章主要為大家描述說明了介紹了spring?boot誕生的背景以及其產(chǎn)生的優(yōu)勢(shì)影響,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2022-03-03spring-boot中spring-boot-maven-plugin報(bào)紅錯(cuò)誤及解決
這篇文章主要介紹了spring-boot中spring-boot-maven-plugin報(bào)紅錯(cuò)誤及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03RocketMQ整合SpringBoot實(shí)現(xiàn)生產(chǎn)級(jí)二次封裝
本文主要介紹了RocketMQ整合SpringBoot實(shí)現(xiàn)生產(chǎn)級(jí)二次封裝,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06springboot jackson自定義序列化和反序列化實(shí)例
這篇文章主要介紹了spring boot jackson自定義序列化和反序列化實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10JVM內(nèi)存模型/內(nèi)存空間:運(yùn)行時(shí)數(shù)據(jù)區(qū)
這篇文章主要介紹了JVM內(nèi)存模型/內(nèi)存空間的相關(guān)資料,幫助大家更好的理解和學(xué)習(xí)Java虛擬機(jī),感興趣的朋友可以了解詳細(xì),希望能夠給你帶來幫助2021-08-08static關(guān)鍵字有何魔法?竟讓Spring Boot搞出那么多靜態(tài)內(nèi)部類(推薦)
這篇文章主要介紹了static關(guān)鍵字有何魔法?竟讓Spring Boot搞出那么多靜態(tài)內(nèi)部類,本文通過實(shí)例代碼圖文相結(jié)合給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-07-07Intellij Mybatis連接Mysql數(shù)據(jù)庫(kù)
最近在搞android的項(xiàng)目,在開發(fā)過程中遇到了好多問題,今天小編給大家說下mybatis連接MySQL數(shù)據(jù)庫(kù)的方法,感興趣的朋友跟著小編一起學(xué)習(xí)吧2016-10-10