統(tǒng)一緩存帝國 - 實(shí)戰(zhàn) Spring Cache
一、揭開 Spring Cache 的面紗
1.1 現(xiàn)有緩存方案的痛點(diǎn)
試想一種場(chǎng)景:
1.用戶 A 打開 APP,進(jìn)入到了秒殺商品的詳情頁,那這個(gè)商品數(shù)據(jù)我們會(huì)先去數(shù)據(jù)庫查詢,然后返回給客戶端。
2.因?yàn)橛写罅坑脩舳虝r(shí)間內(nèi)進(jìn)入到了詳情頁,所以可以把活動(dòng)列表緩存起來,直接讀緩存就可以了。
3.那下次再查詢商品時(shí),直接去緩存查詢就可以了。如果秒殺商品下架了,緩存的數(shù)據(jù)不會(huì)用到了,就把緩存刪掉就可以了。
4.上面幾步看起來也沒啥問題,但是放緩存,刪除緩存這兩步是需要我們?nèi)ナ謩?dòng)寫代碼實(shí)現(xiàn)的。有沒有一種方式不用寫操作緩存的代碼?
5.假如現(xiàn)在用的緩存中間件是 Redis,領(lǐng)導(dǎo)說要換成 Ehcache,操作緩存的代碼是不是又得重新擼一遍?
總結(jié)下上面場(chǎng)景的痛點(diǎn):
需要手寫操作緩存代碼,如添加緩存、更新緩存、刪除緩存。
切換緩存組件并不容易,或者說沒有對(duì)緩存層進(jìn)行抽象封裝,依賴具體的緩存中間件。
哪有沒有一種方案可以幫助解決上面的兩個(gè)痛點(diǎn)呢?
這就是今天要介紹的 Spring Cache。
1.2 Spring Cache 介紹
Spring Cache 是 Spring 提供的一整套的緩存解決方案。雖然它本身并沒有提供緩存的實(shí)現(xiàn),但是它提供了一整套的接口和代碼規(guī)范、配置、注解等,這樣它就可以整合各種緩存方案了,比如 Redis、Ehcache,我們也就不用關(guān)心操作緩存的細(xì)節(jié)。
Spring 3.1 開始定義了 org.springframework.cache.Cache 和 org.springframework.cache.CacheManager 接口來統(tǒng)一不同的緩存技術(shù),并支持使用注解來簡(jiǎn)化我們開發(fā)。
Cache 接口它包含了緩存的各種操作方式,同時(shí)還提供了各種xxxCache緩存的實(shí)現(xiàn),比如 RedisCache 針對(duì)Redis,EhCacheCache 針對(duì) EhCache,ConcurrentMapCache 針對(duì) ConCurrentMap,具體有哪幾種,后面實(shí)戰(zhàn)中會(huì)介紹。
1.3 Spring Cache 有什么功效
每次調(diào)用某方法,而此方法又是帶有緩存功能時(shí),Spring 框架就會(huì)檢查指定參數(shù)的那個(gè)方法是否已經(jīng)被調(diào)用過,如果之前調(diào)用過,就從緩存中取之前調(diào)用的結(jié)果;如果沒有調(diào)用過,則再調(diào)用一次這個(gè)方法,并緩存結(jié)果,然后再返回結(jié)果,那下次調(diào)用這個(gè)方法時(shí),就可以直接從緩存中獲取結(jié)果了。
1.4 Spring Cache 的原理是什么?
Spring Cache 主要是作用在類上或者方法上,對(duì)類中的方法的返回結(jié)果進(jìn)行緩存。那么如何對(duì)方法增強(qiáng),來實(shí)現(xiàn)緩存的功能?
學(xué)過 Spring 的同學(xué),肯定能一下子就反應(yīng)過來,就是用 AOP(面向切面編程)。
面向切面編程可以簡(jiǎn)單地理解為在類上或者方法前加一些說明,就是我們常說的注解。
Spring Cache 的注解會(huì)幫忙在方法上創(chuàng)建一個(gè)切面(aspect),并觸發(fā)緩存注解的切點(diǎn)(poinitcut),聽起來太繞了,簡(jiǎn)單點(diǎn)說就是:Spring Cache 的注解會(huì)幫忙在調(diào)用方法之后,去緩存方法調(diào)用的最終結(jié)果,或者在方法調(diào)用之前拿緩存中的結(jié)果,或者刪除緩存中的結(jié)果,這些讀、寫、刪緩存的臟活都交給 Spring Cache 來做了,是不是很爽,再也不用自己去寫緩存操作的邏輯了。
1.5 緩存注解
Spring 提供了四個(gè)注解來聲明緩存規(guī)則。@Cacheable,@CachePut,@CacheEvict,@Caching。
大家先有個(gè)概念,后面我們?cè)賮砜丛趺词褂眠@些緩存注解。
二、使用緩存
2.1 引入 Spring Cache 依賴
在 pom 文件中引入 spring cache 依賴,如下所示:
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-cache</artifactId>
- </dependency>
2.2 配置使用哪種緩存
Spring Cache 支持很多緩存中間件作為框架中的緩存,總共有 9 種選擇:
- caffeine:Caffeine 是一種高性能的緩存庫,基于 Google Guava。
- couchbase:CouchBase是一款非關(guān)系型JSON文檔數(shù)據(jù)庫。
- generic:由泛型機(jī)制和 static 組合實(shí)現(xiàn)的泛型緩存機(jī)制。
- hazelcast:一個(gè)高度可擴(kuò)展的數(shù)據(jù)分發(fā)和集群平臺(tái),可用于實(shí)現(xiàn)分布式數(shù)據(jù)存儲(chǔ)、數(shù)據(jù)緩存。
- infinispan:分布式的集群緩存系統(tǒng)。
- jcache:JCache 作為緩存。它是 JSR107 規(guī)范中提到的緩存規(guī)范。
- none:沒有緩存。
- redis:用 Redis 作為緩存
- simple:用內(nèi)存作為緩存。
mark
我們還是用最熟悉的 Redis 作為緩存吧。配置 Redis 作為緩存也很簡(jiǎn)單,在配置文件 application.properties 中設(shè)置緩存的類型為 Redis 就可以了, 如下所示:
當(dāng)然,別忘了還要在 pom 文件中 引入 Redis 的依賴,不然用不了 Redis。
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- </dependency>
2.3 測(cè)試緩存
那基礎(chǔ)的配置已經(jīng)做好了,現(xiàn)在就是看怎么使用 Spring Cache 了。
(1)啟動(dòng)類上添加 @EnableCaching注解。本文案例就是在 啟動(dòng)類 PassjavaQuestionApplication 添加 @EnableCaching注解。
(2)指定某方法開啟緩存功能。在方法上添加 @Cacheable 緩存注解就可以了。
@Cacheable 注解中,可以添加四種參數(shù):value,key,condition,unless。首先我們來看下 value 參數(shù)。
下面的代碼出于演示作用,用了最簡(jiǎn)單的邏輯,test 方法直接返回一個(gè)數(shù)字,連數(shù)據(jù)庫查詢都沒有做。不過沒關(guān)系,我們主要驗(yàn)證 Spring Cache 是否對(duì)方法的結(jié)果進(jìn)行了緩存。
- @RequestMapping("/test")
- @Cacheable({"hot"})
- public int test() {
- return 222;
- }
大家注意,@Cacheable 注解中小括號(hào)里面還含有大括號(hào),大括號(hào)里面還有 “hot” 字符串,這個(gè) hot 字符串你可以把它當(dāng)作一個(gè)緩存的名字,然后將 test 方法返回的結(jié)果存到 hot 緩存中。我們也可以用 value="hot" 的方式。
第一次調(diào)用 test 方法前,既沒有 hot 緩存,更沒有 test 的結(jié)果緩存。
調(diào)用 test 方法后,Redis 中就創(chuàng)建出了 hot 緩存了,然后緩存了一個(gè) key,如下圖所示:
第二次調(diào)用 test 方法時(shí),就從緩存 hot 中將 test 方法緩存的結(jié)果 222 取出來了,為了驗(yàn)證沒有執(zhí)行 test 中的方法,大家可以在 test 方法中打下 log 或者斷點(diǎn)。最后的驗(yàn)證結(jié)果肯定是沒有走 test 方法的,而是直接從緩存中獲取的。
那我們?cè)賮頊y(cè)試一個(gè)方法,方法名改為 test2,且請(qǐng)求路徑也改為 test2 了。
- @RequestMapping("/test2")
- @Cacheable({"hot"})
- public int test2() {
- return 456;
- }
大家覺得這兩個(gè)方法的結(jié)果都會(huì)緩存嗎?還是只會(huì)緩存第一個(gè)被調(diào)用的方法?
經(jīng)過測(cè)試,執(zhí)行第一個(gè) test 方法后,再執(zhí)行 test2 方法,緩存結(jié)果一直是 222 不會(huì)變。因?yàn)樗麄兊?key 都是 默認(rèn)的 SimpleKey[],所以兩個(gè)方法對(duì)應(yīng)的緩存的 key 都叫這個(gè),所以得到的緩存值是一樣的。
(3)加上數(shù)據(jù)庫查詢的測(cè)試。
有的同學(xué)可能覺得上面的測(cè)試太簡(jiǎn)單了,test 方法里面啥都沒做,還緩存啥呢,完全沒必要啊。沒關(guān)系,大家的顧慮是對(duì)的,我們來加上數(shù)據(jù)庫查詢,安排~
先說下場(chǎng)景:前端需要查詢某個(gè)題目的詳情,正常邏輯是查詢數(shù)據(jù)庫后返回結(jié)果。假定這個(gè)查詢操作非常頻繁,我們需要將題目詳情進(jìn)行緩存。我們先看看常規(guī) Redis 緩存方案:
先從 Redis 緩存中查看緩存中是否有該題目,如果緩存中有,則返回緩存中的題目;如果沒有,就從數(shù)據(jù)庫中查。查詢出題目后,就用 Redis 存起來,然后返回。這里就要寫操作 Redis 的代碼了:查詢 Redis 緩存、更新 Redis 緩存。
- // 查詢緩存,假定該題目詳情緩存的 key=question1
- redisTemplate.opsForValue().get("question1");
- // 更新緩存
- redisTemplate.opsForValue().set("question1", questionEntity);
那如果用 Spring Cache 注解的話,上面兩行代碼可以直接干掉了。如下所示,加一個(gè) @Cacheable 注解搞定。
- @Cacheable({"question", "hot"})
- public QuestionEntity info(Long id) {
- return getById(id); // 查詢數(shù)據(jù)庫操作
- }
其中 question 和 hot 是緩存的名字,我們可以將結(jié)果放到不同的緩存中。
結(jié)論:
- 如果沒有指定請(qǐng)求參數(shù),則緩存生成的 key name,是默認(rèn)自動(dòng)生成的,叫做 SimpleKey[]。
- 如果指定了請(qǐng)求參數(shù),則緩存的 key name 就是請(qǐng)求參數(shù),比如上面 info 方法,key 等于我傳入的 id = 1。
- 緩存中 key 對(duì)應(yīng)的 value 默認(rèn)使用 JDK 序列化后的數(shù)據(jù)。
- value 的過期時(shí)間為 -1,表示永不過期。
2.4 自定義配置類
上面保存的緩存數(shù)據(jù)都是默認(rèn)設(shè)置,我們也可以自己定義配置,如下所示,在配置文件 application.properties 添加如下配置:
- # 使用 Redis 作為緩存組件
- spring.cache.type=redis
- # 緩存過期時(shí)間為 3600s
- spring.cache.redis.time-to-live=3600000
- # 緩存的鍵的名字前綴
- spring.cache.redis.key-prefix=passjava_
- # 是否使用緩存前綴
- spring.cache.redis.use-key-prefix=true
- # 是否緩存控制,防止緩存穿透
- spring.cache.redis.cache-null-values=true
然后需要加一個(gè)配置類:MyCacheConfig。可以在我的開源項(xiàng)目 passjava 獲取完整源碼。
- RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
- }
2.5 自定義 key
然后我們可以指定 key 的值,可以在 @Cacheable 注解里面加上 key 的值 #root.method.name。這是一種特有的表達(dá)式,稱作 SpEL 表達(dá)式,這里代表用方法名作為緩存 key 的名字。
- @Cacheable(value = {"hot"}, key = "#root.method.name")
接下來就是見證奇跡的時(shí)刻,調(diào)用 test 方法和 test2 方法,發(fā)現(xiàn)有兩個(gè)不同的 key,一個(gè)是 passjava_test1,另外一個(gè) passjava_test2,它們的 key 就是前綴 passjava_ + 方法名 組成。
SpEL 表達(dá)式還有很多其它規(guī)則,如下所示:
可以根據(jù)項(xiàng)目需要選擇合適的表達(dá)式來自定義 key。
2.6 自定義條件
除了設(shè)置緩存條目的 key,我們還可以自定義條件來決定是否將緩存功能關(guān)閉。這里就要用到@Cacheable 另外兩個(gè)屬性:condition 和 unless,它倆的格式還是用 SpEL 表達(dá)式。對(duì)應(yīng)的四個(gè)屬性總結(jié)如下:
代碼示例如下:
- @Cacheable(value = "hot", unless = "#result.message.containss('NoCache')")
當(dāng)放回的結(jié)果 message 字段包含有 NoCache 就不會(huì)進(jìn)行緩存。
2.7 更新注解
@CachePut 也是用來更新緩存,和 @Cacheable 非常相似,不同點(diǎn)是 @CachePut 注解的方法始終都會(huì)執(zhí)行,返回值也會(huì)也會(huì)放到緩存中。通常用在保存的方法上。
保存成功后,可以將 key 設(shè)置保存實(shí)例的 id。這個(gè)怎么做呢?
之前我們說過 key 可以通過 SpEL 表達(dá)式來指定,這里就可以搭配 #result.id 來實(shí)現(xiàn)。
這里還是用個(gè)例子來說明用法:創(chuàng)建題目的方法,返回題目實(shí)例,其中包含有題目 id。
- @RequestMapping("/create")
- @CachePut(value = "hot", key = "#result.id")
- public QuestionEntity create(@Valid @RequestBody QuestionEntity question){
- return IQuestionService.createQuestion(question);
- }
保存的 id 是自增的,值為 123,所以緩存中的 key = passjava_123。
2.8 刪除緩存注解
@CacheEvict 注解的方法在調(diào)用時(shí)不會(huì)在緩存中添加任何東西,但是會(huì)從從緩存中移除之前的緩存結(jié)果。
示例代碼如下:
- @RequestMapping("/remove/{id}")
- @CacheEvict(value = "hot")
- public R remove(@PathVariable("id") Long id){
- IQuestionService.removeById(id);
- return R.ok();
- }
刪除條目的 key 與傳遞進(jìn)來的 id 相同。我測(cè)試的時(shí)候傳的 id = 123,經(jīng)過前綴passjava_組裝后就是 passjava_123,所以將之前緩存的 passjava_123 刪除了。重復(fù)執(zhí)行也不會(huì)報(bào)錯(cuò)。
注意:@CacheEvict 和 @Cacheable、@CachePut 不同,它能夠應(yīng)用在返回值為 void 的方法上。
@CacheEvict 還有些屬性可供使用,總結(jié)如下:
三、 總結(jié)
本文通過傳統(tǒng)使用緩存的方式的痛點(diǎn)引出 Spring 框架中的 Cache 組件。然后詳細(xì)介紹了 Spring Cache 組件的用法:
- 五大注解。@Cacheable、@CachePut、@CacheEvict、@Caching,、@CacheConfig。
- 如何自定義緩存條目的 key。
- 如何自定義 Cache 配置。
- 如何自定義緩存的條件。
當(dāng)然 Spring Cache 并不是萬能的,緩存一致性問題依舊存在,下一篇,我們?cè)偌?xì)聊緩存的一致性問題。
本文轉(zhuǎn)載自微信公眾號(hào)「悟空聊架構(gòu)」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系悟空聊架構(gòu)公眾號(hào)。








































