Spring boot cache 多級(jí)緩存

Spring boot cache 多級(jí)緩存

==本文采用Spring boot cache + Caffenine + Redisson + redis 實(shí)現(xiàn)二級(jí)緩存,拆箱即可用。可做到零配置。==

筆者一直想通過(guò)Caffenine + Redis 實(shí)現(xiàn)二級(jí)緩存,卻在不經(jīng)意期間發(fā)現(xiàn)Spring boot cache 能與Caffenine集成,于是便想將這三者集成在一起。于是研究了一晚上的源碼,通過(guò)復(fù)制Spring boot cache開(kāi)啟攔截的方式,強(qiáng)行在spring boot cache攔截之后以相同的方式加了一層Redis緩存,但仍無(wú)法實(shí)現(xiàn)Caffenine的refreshAfterWrit配置,如要實(shí)現(xiàn)得做較大改造,且并不好用。refreshAfterWrit的好處是:如果刷新時(shí)間到了且緩存還沒(méi)過(guò)期,便會(huì)返回舊值,開(kāi)啟新的線(xiàn)程去更新緩存。無(wú)奈之下只好到處查找相關(guān)文章。于是找到了這篇文章:SpringBoot+SpringCache實(shí)現(xiàn)兩級(jí)緩存(Redis+Caffeine) 。因?yàn)樘^(guò)于復(fù)雜,所以只是瞟了一眼,但發(fā)現(xiàn)了作者在結(jié)尾寫(xiě)的擴(kuò)展,可以通過(guò)redisson增加一級(jí)緩存,于是便有了這個(gè)想法:將Spring boot cache、Caffenine、 Redisson、redis一起集成。然而過(guò)程并不順利,繼續(xù)不斷查找文獻(xiàn),通過(guò)該文獻(xiàn)Redisson和Spring Cache框架整合使用發(fā)現(xiàn)我想要的所有功能都已經(jīng)實(shí)現(xiàn),RedissonClusteredSpringLocalCachedCacheManager與RedissonSpringClusteredLocalCachedCacheManager已經(jīng)實(shí)現(xiàn)了我想要的功能。但很不幸的是redisson.pro為商業(yè)收費(fèi)版,找不到相關(guān)資源。無(wú)賴(lài)之下害的自己實(shí)現(xiàn)。于是復(fù)制了他的類(lèi)名RedissonClusteredSpringLocalCachedCacheManager(懶的取名字),自己寫(xiě)了個(gè)CacheManager。放棄了使用數(shù)據(jù)分片功能,原因也很簡(jiǎn)單-用不到。因?yàn)榇蠖鄶?shù)發(fā)布生產(chǎn)環(huán)境會(huì)采購(gòu)云服務(wù),以阿里云Redis集群版來(lái)說(shuō),阿里云提供了代理連接,通過(guò)代理實(shí)現(xiàn)了數(shù)據(jù)分片功能,數(shù)據(jù)怎么路由怎么存儲(chǔ)都由代理處理過(guò)了。當(dāng)然也可以開(kāi)通直連功能,這時(shí)候就需要配置Redis多個(gè)節(jié)點(diǎn)。多一事不如少一事,有代理連接干嘛非得找事呢。

1、多級(jí)緩存的好處是什么

可以減少對(duì)redis的訪(fǎng)問(wèn)提高響應(yīng)數(shù)度,除此之外也能很好的解決redis的緩存穿透、緩存擊穿、緩存雪崩問(wèn)題。另外本地緩存可以選擇多種淘汰策略,比如使用LFU策略用來(lái)解決熱點(diǎn)數(shù)據(jù)問(wèn)題。

2、本地緩存策略

緩存淘汰策略參考的是該文獻(xiàn):真正的緩存之王,Google Guava 只是弟弟

筆者在該文獻(xiàn)中出現(xiàn)了兩處錯(cuò)誤:

  1. 第一處:文章指出:“refreshAfterWrite配置必須指定一個(gè)CacheLoader”??紤]到與Spring boot cache集成,并不能簡(jiǎn)單注冊(cè)一個(gè)CacheLoader就能使用,原碼中的CaffeineCacheManager已滿(mǎn)足不了需求,需要?jiǎng)?chuàng)建一個(gè)新的CaffeineCacheManager,在manager中為每一個(gè)命名空間適配一個(gè)CacheLoader,然后在CacheLoader的load方法中調(diào)用對(duì)應(yīng)service中的查詢(xún)方法,做到這個(gè)程度要改動(dòng)的代碼不少。
  2. 第二處:為@Cacheable與@CachePut的使用,一個(gè)用于查詢(xún)一個(gè)用于修改,錯(cuò)誤點(diǎn)是方法沒(méi)有返回值,都是用的void類(lèi)型。這會(huì)造成aop代理后無(wú)法獲取返回值,導(dǎo)致緩存中存儲(chǔ)的是null。通常查詢(xún)都會(huì)修改void為具體對(duì)象,但會(huì)存在一部分人并不會(huì)給修改的方法添加返回值。

(也許文章作者僅是以偽代碼作為示意,但這兩處不夠嚴(yán)謹(jǐn),足以讓不熟悉該組件的人為此掉不少頭發(fā))

2.1、緩存淘汰策略

  1. LRU:最近最少使用算法,每次訪(fǎng)問(wèn)數(shù)據(jù)都會(huì)將其放在我們的隊(duì)尾,如果需要淘汰數(shù)據(jù),就只需要淘汰隊(duì)首即可。仍然有個(gè)問(wèn)題,如果有個(gè)數(shù)據(jù)在 1 分鐘訪(fǎng)問(wèn)了 1000次,再后 1 分鐘沒(méi)有訪(fǎng)問(wèn)這個(gè)數(shù)據(jù),但是有其他的數(shù)據(jù)訪(fǎng)問(wèn),就導(dǎo)致了我們這個(gè)熱點(diǎn)數(shù)據(jù)被淘汰。
  2. LFU:最近最少頻率使用,利用額外的空間記錄每個(gè)數(shù)據(jù)的使用頻率,然后選出頻率最低進(jìn)行淘汰。這樣就避免了 LRU 不能處理時(shí)間段的問(wèn)題。

緩存策略各有利弊,實(shí)現(xiàn)的成本也是一個(gè)比一個(gè)高,同時(shí)命中率也是一個(gè)比一個(gè)好。Guava Cache雖然有這么多的功能,但是本質(zhì)上還是對(duì)LRU的封裝,如果有更優(yōu)良的算法,并且也能提供這么多功能,相比之下就相形見(jiàn)絀了。

LFU的局限性 :在 LFU 中只要數(shù)據(jù)訪(fǎng)問(wèn)模式的概率分布隨時(shí)間保持不變時(shí),其命中率就能變得非常高。比如有部新劇出來(lái)了,我們使用 LFU 給他緩存下來(lái),這部新劇在這幾天大概訪(fǎng)問(wèn)了幾億次,這個(gè)訪(fǎng)問(wèn)頻率也在我們的 LFU 中記錄了幾億次。但是新劇總會(huì)過(guò)氣的,比如一個(gè)月之后這個(gè)新劇的前幾集其實(shí)已經(jīng)過(guò)氣了,但是他的訪(fǎng)問(wèn)量的確是太高了,其他的電視劇根本無(wú)法淘汰這個(gè)新劇,所以在這種模式下是有局限性。

LRU的優(yōu)點(diǎn)和局限性 :LRU可以很好的應(yīng)對(duì)突發(fā)流量的情況,因?yàn)樗恍枰塾?jì)數(shù)據(jù)頻率。但LRU通過(guò)歷史數(shù)據(jù)來(lái)預(yù)測(cè)未來(lái)是局限的,它會(huì)認(rèn)為最后到來(lái)的數(shù)據(jù)是最可能被再次訪(fǎng)問(wèn)的,從而給與它最高的優(yōu)先級(jí)。

3、使用方式(回歸正題)

3.1、 引入pom依賴(lài)

zjun-cache-spring-boot-starter 使用了自動(dòng)化配置,拆箱即可使用。

<!--已發(fā)布至oss.sonatype.org公共倉(cāng)庫(kù)-->
<!--SNAPSHOT版-->
<dependency>
  <groupId>io.github.zjun02</groupId>
  <artifactId>zjun-cache-spring-boot-starter</artifactId>
  <version>0.0.1-SNAPSHOT</version>
</dependency>
<!--release版-->
<dependency>
  <groupId>io.github.zjun02</groupId>
  <artifactId>zjun-cache-spring-boot-starter</artifactId>
  <version>0.0.1-release</version>
</dependency>

3.2、 service 中使用

用法參照Spring boot cache。相關(guān)文獻(xiàn):SpringBoot2.x—SpringCache使用

@Slf4j
@Service
// 指定了兩個(gè)命名空間 demo、test。只需一個(gè)命名空間即可,此處僅為演示
@CacheConfig(cacheNames = {"demo", "test"})
public class QueryService {
    
    private static int count = 0;

    @Cacheable(key = "#keyWord")
    public String query(String keyWord) {
        log.info("查詢(xún)key: {}", keyWord);
        String queryResult = doQuery(keyWord);
        return queryResult;
    }
    
    @CachePut(key = "#keyWord")
    public String put(String keyWord, String value) {
         log.info("修改key: {}", keyWord);
         String queryResult = doQuery(value);
         return queryResult;
    }
    
    @CacheEvict(key = "#keyWord")
    public String remote(String keyWord) {
        log.info("刪除key: {}", keyWord);
        return keyWord;
    }

    private String doQuery(String keyWord) {
        try {
            Thread.sleep(100L);
            String result = keyWord + "-" + ++count;
            return result;
        } catch (InterruptedException e) {
            throw new IllegalStateException(e);
        }
    }
}

3.3、 緩存命名空間配置

在src/main/resources目錄下創(chuàng)建配置文件zjun-cache-config.yaml,配置文件非必需,如沒(méi)有則將使用默認(rèn)配置

demo: // 命名空間
  ttl: 30000 // 鍵值條目的存活時(shí)間,以毫秒為單位。
  maxIdleTime: 720000 // 鍵值輸入的最大空閑時(shí)間(毫秒)。
  maxSize: 100 // 緩存容量
  mode: LFU // 非多級(jí)緩存時(shí)生效。有效值: LRU、LFU

// 注意開(kāi)啟多級(jí)緩存后 ttl與maxIdleTime僅針對(duì)本地緩存生效,redis中為永久保存
test: // 命名空間
  ttl: 30000 // 鍵值條目的存活時(shí)間,以毫秒為單位。
  maxIdleTime: 720000 // 鍵值輸入的最大空閑時(shí)間(毫秒)。
  maxSize: 100 // 本地緩存容量 
  multistage: true // 開(kāi)啟多級(jí)緩存,默認(rèn)為false
  evictionPolicy: LRU // 本地緩存策略。有效值:NONE、LRU、LFU、SOFT、WEAK
  cacheProvider: CAFFEINE // 本地緩存技術(shù)選型。有效值:REDISSON、CAFFEINE。默認(rèn)值:REDISSON
  

注意 如切換緩存淘汰策略請(qǐng)先清理redis中的數(shù)據(jù),否則可能獲取緩存時(shí)會(huì)出現(xiàn)異常

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容