1. 程式人生 > 其它 >SpringBoot 快取註解的使用

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註解,解析一下:

  1. value="cache_1d"是我定義的快取值,這個配置設定了快取1天。
  2. 自定義快取key,key為方法名。同樣可以使用keyGenerator設定key生成策略,不過我覺得使用spel表示式更加靈活,如果使用keyGenerator,keyGenerator的實現類需要實現org.springframework.cache.interceptor.KeyGenerator介面,具體的實現方法實現在generate方法中。
  3. unless的意思就是如果返回結果是null陣列或陣列大小是0,則不快取此結果。
  4. condition則判定了是否走換成邏輯,如果skip是null,即condition是false,就不去讀取快取,而是直接執行目標方法。
  5. 如過有需求,你們可以指定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、更多還在你的實踐,有錯誤歡迎指導,有擴充套件歡迎大家評論。

好了,這個就到此結束了,歡迎大家給出意見。發表自己的建議,如需要補充,評論去也同時可見。拜拜。
想當初是打算一週一篇文章的,但是不太好搞啊,真的是腦袋本,面對著電腦想不出應該怎麼說好這一句話,寫了刪,然後再寫,諾,現在才寫成這麼樣子,真羨慕那些文筆不錯的同學,能夠出口成章準確表達出自己的意思也能夠讓他人更好的理解。
給自己拉個贊吧:歡迎點贊關注和評論,更希望本篇文章你看完之後有所收穫。