SpringBoot 快取註解的使用
最近比較忙,沒時間更新了。上一篇文章我說了如何使用Redis做快取,文末我稍微提到了SpringBoot對快取的支援。本篇文章就針對SpringBoot說一下如何使用。
1、SpringBoot對快取的支援
SpringBoot對快取的支援我們需要引入包:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <!-- 如果需要整合redis,需要再加入redis包 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.4.2</version> </dependency>
快取的支援是依靠介面:org.springframework.cache.annotation.CachingConfigurer的實現。所以我們使用SpringBoot自定義快取只需要實現CachingConfigurer介面,並給出合理的實現即可。所以我們通常使用快取如Ecache,Redis等較好的快取框架都是已經實現了的。預設情況下SpringBoot同樣是使用本地快取,可以通過配置檔案配置快取配置項。具體如何配置請參考我的上篇文章:如何使用Redis做快取
SpringBoot為我們做了很多事情,我們使用快取只需要瞭解3個註解:@Cacheable, @CachePut, @CacheEvict。Cacheable作用是讀取快取,CachePut是放置快取,CacheEvict作用是清除快取。
2、Cacheable註解
這個註解我們會相對熟悉,在上一篇文章中我們就說過這個註解的使用,這裡再複製貼上一下:
/** * 就直接佔著原始碼說了 */ @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Cacheable { /** * 設定使用換成的名稱,這兩個值是一樣的,我們通過這個值來區分不同快取的配置 * 比如我們可以設定不同的cacheName來設定快取時間、設定不同的key生成策略等。 */ @AliasFor("cacheNames") String[] value() default {}; @AliasFor("value") String[] cacheNames() default {}; /** * 設定key生成策略,支援spel表示式,且存在root方法,可以通過#root.method,#root.target等使用root物件 * 同樣可以固定快取key為固定字串。 * 如果不設定,SpringBoot提供預設的key生成策略。 */ String key() default ""; /** * 在註解中指定key生成器 */ String keyGenerator() default ""; /** * 在註解中制定此註解使用的快取管理器 */ String cacheManager() default ""; /** * 在註解中指定此註解的快取解析器,和快取管理器互斥 */ String cacheResolver() default ""; /** * 設定使用快取條件,使用Spel表示式解析。如果表示式返回false,則這次執行不走快取邏輯 */ String condition() default ""; /** * 設定不快取條件,Spel表示式。如果表示式返回true,則不對快取結果進行快取。 * 這個和condition()的區別是在於執行時機不同,condition方法是在執行方法前呼叫,而unless方法是在執行目標方法後呼叫。 */ String unless() default ""; /** * 採用同步的方式,預設false */ boolean sync() default false; }
此註解主要描述快取的使用策略
舉個栗子:
public static final String D1 = "cache_1d";
@Cacheable(value = CacheTimes.D1, key = "#root.methodName", unless = "#result == null || #result.size() < 1", condition = "#skip != null")
public List<String> getList(String skip) {
return Arrays.stream(UUID.randomUUID().toString().split("-")).collect(Collectors.toList());
}
上述例子使用了@Cacheable註解,解析一下:
- value="cache_1d"是我定義的快取值,這個配置設定了快取1天。
- 自定義快取key,key為方法名。同樣可以使用keyGenerator設定key生成策略,不過我覺得使用spel表示式更加靈活,如果使用keyGenerator,keyGenerator的實現類需要實現org.springframework.cache.interceptor.KeyGenerator介面,具體的實現方法實現在generate方法中。
- unless的意思就是如果返回結果是null陣列或陣列大小是0,則不快取此結果。
- condition則判定了是否走換成邏輯,如果skip是null,即condition是false,就不去讀取快取,而是直接執行目標方法。
- 如過有需求,你們可以指定cacheManager或cacheResolver。比如預設使用的是RedisCacheManager,然而某個快取不需要使用Redis即可,可以單獨使用Ecache,則可以在註解中指定Ecache的cachemanager。
3、CachePut註解
上面我們說CachePut註解作用是放置快取。有點彆扭,意思就是CachePut註解能夠設定一些滿足條件的快取。雖然說我們通常用它來更新快取(假如使用Redis做快取,可以用它來設定redis的值。雖然能實現,但是還是不建議用這個去設定Redis的值哈,因為那樣使用違反了註解的本身的意思,別人看了種以為它是快取咋辦),但我認為不能說成更新快取,更新的意思是必須要有然後改變其值。
CachePut的原始碼和Cacheable基本一致
public @interface CachePut {
@AliasFor("cacheNames")
String[] value() default {};
@AliasFor("value")
String[] cacheNames() default {};
String key() default "";
String condition() default "";
String unless() default "";
String keyGenerator() default "";
String cacheManager() default "";
String cacheResolver() default "";
}
同理Cacheable,CachePut註解生效是在滿足condition()true和unless()false的情況。
用法舉栗子
@CachePut(value = CacheTimes.D1, key = "#root.methodName", unless = "#result == null || #result.size() < 1", condition = "#condition" )
public List<String> setListCache(List<String> list, boolean condition){
if(list != null && list.size() > 0){
return list;
}
return Arrays.stream(UUID.randomUUID().toString().split("-")).collect(Collectors.toList());
}
上例中unless和condition和Cacheable用法一致,不同的就是CachePut註解的方法被呼叫時不會去讀取快取中儲存的資料,只是在呼叫結束判斷是否將資料寫入快取。即滿足引數condition=true,return value is not NullList,就會以key=setListCache將執行結果存入快取中。
如果key設定和Cacheable快取的key相同,那麼方法呼叫結束就會更新快取。
-
千奇百怪:使用CachePut給Redis賦值(其實就是湊湊字數,既然是快取的註解,那麼咱們還是隻用來做快取更好,並且,快取也不一定用的就是Redis,哈哈哈哈哈哈哈)
@CachePut(value = CacheTimes.D1, key = "#key")
public Object setKV(String key, Object value){
return value;
}
如果你這樣使用了,那麼應要注意你的value這個快取的過期時間,和CacheEvict註解的使用。。。。
4、CacheEvict註解
CacheEvict註解在相對於前兩個註解,多了兩個屬性:allEntries和beforeInvocation
public @interface CacheEvict {
/** 是否刪除所有快取 */
boolean allEntries() default false;
/** 在方法執行前/後進行刪除快取操作 */
boolean beforeInvocation() default false;
@AliasFor("cacheNames")
String[] value() default {};
@AliasFor("value")
String[] cacheNames() default {};
String key() default "";
String condition() default "";
String keyGenerator() default "";
String cacheManager() default "";
String cacheResolver() default "";
}
引數:allEntries
allEntries引數作用是刪除所有快取資料,預設是false,讓我們指定key去刪除匹配的快取。詳細看下面兩個例子:
- 刪除全部快取資料
@CacheEvict(value = CacheTimes.D1, condition = "#condition", allEntries = true)
public void clearCache(boolean condition){
……
}
上面的方法的意思就是說當引數condition=true時,CacheEvict註解開始生效,因為allEntries=true,所以會刪除value=CacheTimes.D1下所有的快取資料。
比如之前在CacheTime.D1下設定了快取:"a"="b","c"="c","d"="d",在呼叫了clearCache方法之後,上述快取將全部被刪除。
- 指定key刪除
@CacheEvict(value = CacheTimes.D1, allEntries = false, key = "#key")
public void clearCache(String key){
}
如果在allEntries=false的情況下,CacheEvict將會刪除制定key的鍵值。理應在指定key的情況下,allEntries應當為false;否則指定的key將無效。
那麼當allEntries=true的時候,springboot是怎麼判斷應當刪除哪條資料的呢?
這個我們要先搞清楚快取的key生成策略:預設情況下,快取會拿註解中的value值作為字首+"::"+你自定義的key生成策略作為這個快取的真實key。當我們呼叫CacheEvict註解中allEntries的值為true時,springboot就會根據上述的key生成策略去匹配快取系統中的資料,即以註解中value值為字首的key去刪除。
那麼就會存在這樣一個問題:假如我們重寫了上述的key生成策略,使得快取在生成key時沒有字首,那麼刪除時會發生什麼?
答案就是你想象的那樣:會刪除快取系統中所有的資料,如果快取使用的是redis,那麼redis中所有的資料將被清空。
引數:beforeInvocation
既然是快取,我們就應當更加全面的去操作快取。那麼假設我們對某個User表做了快取,當新增資料時,我們使用Cacheable去設定快取;讀取時同樣根據Cacheable策略去取出資料;當修改時,我們使用CachePut去更新快取;當刪除時,我們理應使用CacheEvict去刪除快取。這時就會存在一個問題:在呼叫方法刪除某條id=123的使用者記錄時,由於業務原因出現異常(是否刪除成功狀態未知),那麼這時,這個快取我們應不應當清理。
這種情況下beforeInvocation給我們了選擇。當beforeInvocation=true時,SpringBoot先進行快取操作(先刪除快取)在執行方法邏輯。當beforeInvocation=false時,先執行方法業務邏輯,再刪除快取。(當然,業務正常執行的時候都無所謂)
當業務出現異常時,我們要麼選擇刪除快取,要麼不刪除。
- 快取操作在前(先刪除快取,再執行邏輯,不管是否異常,快取已經清空)
@CacheEvict(value = CacheTimes.D1, condition = "#condition", beforeInvocation = true, allEntries = true)
public void clearCache(boolean condition){
throw new RuntimeException("exception");
}
- 快取操作在後(預設,出現異常不刪除快取)
@CacheEvict(value = CacheTimes.D1, condition = "#condition",beforeInvocation = false, allEntries = true)
public void clearCache(boolean condition){
throw new RuntimeException("exception");
}
5、Caching註解實現複雜快取邏輯
快取就是讀寫更新,那麼上面三個註解已經夠了,Caching是幹什麼的呢?
public @interface Caching {
Cacheable[] cacheable() default {};
CachePut[] put() default {};
CacheEvict[] evict() default {};
}
Caching註解中包含了Cacheable、CachePut、CacheEvict三個註解。所以我們很應該能夠想象得到,在Caching中能夠使用多個快取註解,主要是為了實現一些複雜的快取邏輯,且不需要在多個方法上去實現。
這個沒啥好說的,簡潔明瞭。
6、常見的(我碰到的)業務實現使用。。。
1、對mysql某個表進行快取
這種邏輯通常不會使用單機快取,而經常使用一個單獨的快取系統:比如Redis、elasticsearch去作為快取,因為要保持資料的一直性。
class user{......}
//儲存資料庫的同時,儲存到快取
@CachePut(value = "user", condition = "#user != null", key = "#user.id")
public User insert(User user){
jdbc.insert(user)
return user;
}
//讀取,如果快取過期,則重新查資料,重新寫入快取
@Cacheable(value = "user", condition = "#userId != null", key = "#userId", unless = "#result != null")
public User select(Integer userId){
User user = jdbc.select(userId);
return user;
}
//刪除資料時同樣清除快取
@CacheEvict(value = "user", condition = "#userId != null", key = "#userId", allEntries = false, beforeInvocation = true)
public void delete(Integer userId){
jdbc.delete(userId);
}
2、對複雜查詢邏輯進行快取
通常我們一個restful介面會存在非常複雜的邏輯,導致介面請求時間過長,這時我們會考慮將介面中的資料做快取,而不是每次進入都重新走一邊業務。而業務多變,在整個介面中設定我們可能不僅僅去讀取快取,或許可以根據引數的不同我們需要同時實現快取的更新和清理。demo如下:
@Caching(
cacheable = {
@Cacheable(value = CacheTimes.D1, key = "#root.methodName + #userId", condition = "#update == null", unless = "#result != null || #result.size() < 1"),
@Cacheable(value = CacheTimes.D7, key = "#root.methodName + #userId", condition = "#update == null", unless = "#result != null", cacheManager = "cacheManager")
},
put = {
@CachePut(value = CacheTimes.D1, key = "#root.methodName + #userId", condition = "#update != null && #update.size() > 0"),
@CachePut(value = CacheTimes.D7, key = "#root.methodName + #userId", condition = "#update != null", cacheManager = "cacheManager")
}
)
public List<Object> recommend(String userId, List<Object> update){
if(update != null && update.size > 0){
return update;
}
List<Object> l1 = selectTable1(userId);
List<Object> l2 = selectTable2(userId);
List<Object> l3 = selectTable3(userId);
List<Object> l4 = selectTable4(userId);
List<Object> l5 = selectTable5(userId);
List<Object> result = new ArrayList();
result.addAll(l1);
result.addAll(l2);
result.addAll(l3);
result.addAll(l4);
result.addAll(l5);
return converVo(result);
}
1、如上述demo,當引數update不是null時我們直接返回了update的值,update不是null觸發了CachePut註解,我們會更新兩個快取,一個是預設的快取更新管理器,一個指定了快取管理器。
2、那麼當符合Cachebale註解的時候呢?我們上面定義了兩個Cacheable註解,當兩個都滿足註解條件,而且兩個快取中的值是不同的,會報異常嗎?當然不會,當出現兩個的時候讀取快取時,SpringBoot會返回你第一個註解的值。
3、如果引數同時符合CachePut和Cacheable的時候呢?通過測試,我發現如果同時慢住這兩個註解的條件,會以CachePut的優先順序更高,所以1. 會更新快取;2. 返回的結果是更新後的快取。
4、如果我再加了個CacheEvict,會不會清空快取?
當然會。
3、更多還在你的實踐,有錯誤歡迎指導,有擴充套件歡迎大家評論。
好了,這個就到此結束了,歡迎大家給出意見。發表自己的建議,如需要補充,評論去也同時可見。拜拜。
想當初是打算一週一篇文章的,但是不太好搞啊,真的是腦袋本,面對著電腦想不出應該怎麼說好這一句話,寫了刪,然後再寫,諾,現在才寫成這麼樣子,真羨慕那些文筆不錯的同學,能夠出口成章準確表達出自己的意思也能夠讓他人更好的理解。
給自己拉個贊吧:歡迎點贊關注和評論,更希望本篇文章你看完之後有所收穫。