溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

怎么使用spring-cache代碼解決緩存擊穿問題

發(fā)布時間:2022-04-24 13:37:39 來源:億速云 閱讀:419 作者:iii 欄目:開發(fā)技術(shù)

本篇內(nèi)容介紹了“怎么使用spring-cache代碼解決緩存擊穿問題”的有關(guān)知識,在實(shí)際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

    正文

    目前缺陷

    首先,為什么說目前網(wǎng)上流傳的方案,落地性差呢,因?yàn)槎既狈σ粋€可以和SpringBoot結(jié)合起來的真實(shí)場景,基本上都脫離了SpringBoot,只站在Java這個層級去分析。那問題就來了,現(xiàn)在還有只用SpringMvc,卻不用SpringBoot的公司么?因此,本文嘗試將該方案和SpringBoot結(jié)合起來,講一個確實(shí)可行,可以落地的方案!

    當(dāng)然,我們先來說說目前在網(wǎng)上流傳的幾套方案,到底不靠譜在哪里!

    (1)布隆過濾器

    關(guān)于布隆過濾器,我就不介紹太多,這里就理解為是一個過濾器,用于快速檢索一個元素是否在一個集合中;那么當(dāng)一個請求來的時候,快速判斷這個請求的key是否在指定集合中!如果在,說明有效,則放行。如果不在,則無效攔截。 至于實(shí)現(xiàn),各大博客也說了用了google提供的

    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>19.0</version>
    </dependency>

    這個包里有現(xiàn)成寫好的java類給你使用了,當(dāng)然demo代碼我就不貼了,一抓一大把! 當(dāng)然,似乎看上去完美無暇!一切都是那么的合適!

    然而到這里,我就真的問一句,你們真的用了這個方案了?

    我如果猜的沒錯,應(yīng)該沒幾個人遇到過緩存擊穿問題~

    更何況,證明這個說法的正確性~

    該方案最大的一個問題是布隆過濾器不支持反向刪除操作,例如你的項(xiàng)目里活躍的key的數(shù)量只有1000w個,但是全部key數(shù)量有5000w個,那這5000w個key會全部存在布隆過濾器里!

    直到某一天,你會發(fā)現(xiàn)這個過濾器太擁擠了,誤判率太高,不得不進(jìn)行重建!

    so,你們覺得這個做法真的靠譜?

    那么布隆過濾器這個說法出自哪里呢? (大家一定很好奇對不對!)

    當(dāng)然是xx機(jī)構(gòu)~~此處保護(hù)自己的狗頭~~記住,他們?yōu)榱烁罹虏?,一定會選擇一些看起來極為高端,但是落地巨不靠譜的方案(這也是區(qū)分一個機(jī)構(gòu)到底是割韭菜還是真正有水平的標(biāo)桿,小白不懂,很容易被坑)~~看到這里,真是慚愧,我的第一篇文章也是寫這個方案了,但是在落地過程中,發(fā)現(xiàn)了不對勁(此處省略一萬字的檢討文,煙哥垃圾~~)。

    (2)布谷過濾器

    那么,為了解決布隆過濾器查詢性能弱、空間利用效率低、不支持反向操作等問題,又有一篇文章誕生了,主張用布谷過濾器來解決緩存擊穿問題!

    但是,神奇的事情來了,基本上所有的文章都在說布谷過濾器多么多么牛逼,卻沒有任何落地的方案~

    記住,我們平時寫代碼,一定是怎么方便怎么來!再記住,面試是一回事,代碼落地是另一回事~

    那,真正簡便的方案是什么樣的呢?來,我們一步步來~

    真正方案

    假設(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ì)上不是一個具體的緩存實(shí)現(xiàn)方案,而是一個對緩存使用的抽象,通過在既有代碼中添加少量自定義的各種annotation,即能夠達(dá)到使用緩存對象和緩存方法的返回對象的效果。Spring的緩存技術(shù)具備相當(dāng)?shù)撵`活性,不僅能夠使用SpEL(Spring Expression Language)來定義緩存的key和各種condition,還提供開箱即用的緩存臨時存儲方案,也支持和主流的專業(yè)緩存集成。

    例如:我們在代碼中經(jīng)常有這么一段邏輯,在目標(biāo)方法執(zhí)行前,會根據(jù)key先去緩存中查詢看是否有數(shù)據(jù),有就直接返回緩存中的key對應(yīng)的value值,不再執(zhí)行目標(biāo)方法;沒有則執(zhí)行目標(biāo)方法,去數(shù)據(jù)庫查詢出對應(yīng)的value,并以鍵值對的形式存入緩存。

    如果我們不使用例如spring-cache的注解框架,你的代碼中會充斥著大量冗余代碼,而用了該框架后,以@Cacheable注解為例, 該注解在方法上,表示該方法的返回結(jié)果是可以緩存的。

    也就是說,該方法的返回結(jié)果會放在緩存中,以便于以后使用相同的參數(shù)調(diào)用該方法時,會返回緩存中的值,而不會實(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;
    }

    在這個例子中,findById 方法與一個名為 menu 的緩存關(guān)聯(lián)起來了。調(diào)用該方法時,會檢查 menu 緩存,如果緩存中有結(jié)果,就不會去執(zhí)行方法了。

    ok,說到這里,其實(shí)都是大家懂得東西??!接下來開始我們的主題:如何解決緩存擊穿問題!順便講講穿透和雪崩問題!

    來來來,我們回憶一下緩存擊穿,穿透以及緩存雪崩的概念!

    緩存穿透

    在高并發(fā)下,查詢一個不存在的值時,緩存不會被命中,導(dǎo)致大量請求直接落到數(shù)據(jù)庫上,如活動系統(tǒng)里面查詢一個不存在的活動。 多嘴一句:緩存穿透是指,請求的是緩存和數(shù)據(jù)庫中都沒有的數(shù)據(jù)!

    對于緩存穿透問題,有一個很簡單的解決方案,就是緩存NULL值~從緩存取不到的數(shù)據(jù),在數(shù)據(jù)庫中也沒有取到,直接返回空值。

    那么spring-cache中,有一個配置是這樣的

    spring.cache.redis.cache-null-values=true

    帶上該配置后,就可以緩存null值了,值得一提的是,這個緩存時間要設(shè)的少一點(diǎn),例如15秒就夠,如果設(shè)置過長,會導(dǎo)致正常的緩存也無法使用。

    緩存擊穿

    在高并發(fā)下,對一個特定的值進(jìn)行查詢,但是這個時候緩存正好過期了,緩存沒有命中,導(dǎo)致大量請求直接落到數(shù)據(jù)庫上,如活動系統(tǒng)里面查詢活動信息,但是在活動進(jìn)行過程中活動緩存突然過期了。 多嘴一句:緩存擊穿是指,請求的是緩存沒有,而數(shù)據(jù)庫中有的數(shù)據(jù)!

    記住,解決擊穿的最簡單的方法,只有一個,就是限流!至于怎么限,其實(shí)可以各顯神通!例如其他文章提到的布隆過濾器,布谷過濾器等,不過是限流方式之一而已!甚至,你用一些其他的限流組件也是可以的!

    這里就要說spring-cahce的另一個配置了!

    在緩存過期之后,如果多個線程同時請求對某個數(shù)據(jù)的訪問,會同時去到數(shù)據(jù)庫,導(dǎo)致數(shù)據(jù)庫瞬間負(fù)荷增高。Spring4.3為@Cacheable注解提供了一個新的參數(shù)“sync”(boolean類型,缺省為false),當(dāng)設(shè)置它為true時,只有一個線程的請求會去到數(shù)據(jù)庫,其他線程都會等待直到緩存可用。這個設(shè)置可以減少對數(shù)據(jù)庫的瞬間并發(fā)訪問。

    看到這里??!這不就是一個限流方案么?

    所以解決方法就是,加一個屬性sync=true,就行。代碼就像下面這樣

    @Cacheable(cacheNames="menu", sync="true")

    用了該屬性后,可以指示底層將緩存鎖住,使只有一個線程可以進(jìn)入計(jì)算,而其他線程堵塞,直到返回結(jié)果更新到緩存中。

    當(dāng)然,看到這里,一定會有人和我抬杠!他的問題是這樣的!

    你這個只是針對單機(jī)的限流,并不是整體集群的限流!也就是說,假設(shè)你的集群搭建了3000個pod,最差的情況下就是,3000個pod上,每個pod都會發(fā)起一個請求去數(shù)據(jù)庫查詢,照樣還是會導(dǎo)致數(shù)據(jù)庫連接數(shù)不夠用,等等資源問題!

    對于這個問題我只能說!少年,但凡你的公司產(chǎn)品達(dá)到這種流量規(guī)模,此刻你就不會在看我的文章!你此刻關(guān)心的問題是:

    (1)哎,買深圳灣一號還是深圳灣公館呢,糾結(jié)!
    (2)昨天美股又跌了,又損失了兩套房
    (3)昨天提前撤單了,又少掙了幾萬
    ....(省略一萬字)

    當(dāng)然,如果你非要解決,也有辦法。spring的aop有套路的,比如@Transactional的Advice是TransactionInterceptor,那么cache也對應(yīng)對一個CacheInterceptor,我們只要去改CacheInterceptor,這個切面就能解決。在里頭做一個分布式鎖!偽代碼如下

    flag := 取分布式鎖
    if flag {
        走數(shù)據(jù)庫查詢,并緩存結(jié)果
    }{
        睡眠一段時間,再次嘗試獲取key的值
    }

    但是,我還是要多嘴提一句,真沒必要~~ 記住一句話,立足實(shí)際出發(fā)~但凡你的業(yè)務(wù)到了那種級別,是可以做到區(qū)域部署的,完全可以規(guī)避開這類問題。

    緩存雪崩

    在高并發(fā)下,大量的緩存key在同一時間失效,導(dǎo)致大量的請求落到數(shù)據(jù)庫上,如活動系統(tǒng)里面同時進(jìn)行著非常多的活動,但是在某個時間點(diǎn)所有的活動緩存全部過期。

    那么針對該問題,最簡單的解決方法就是,過期時間加隨機(jī)值!

    但是很麻煩的是,我們在使用@Cacheable注解的時候,原生功能沒法直接設(shè)置隨機(jī)過期時間的。

    這個老實(shí)說,真沒啥好方法,只能自己繼承RedisCache,對其增強(qiáng),改寫其中的put方法,帶上隨機(jī)時間!

    “怎么使用spring-cache代碼解決緩存擊穿問題”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!

    向AI問一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI