[spring]Spring Cache學習

一、前言

??最近在和同事一塊做一個需求的時候,看了下同事的代碼,在使用緩存這塊看到了Spring Cache這個玩意,感到很陌生。看了下簡介,這玩意從Spring 3.X之后都有了,但我卻一點也不了解,感到很慚愧,于是趕緊花了點時間學習下。

本文已學習注解為主,畢竟Spring Boot這類注解式的框架已逐漸成為了主流。參考的Spring版本是4.3.15。

二、Spring Cache注解

1. 簡介

Spring Cache是Spring3.1之后引入的基于注解的緩存方案,旨在通過少量的注解,達到能完成緩存功能的效果。其中涉及到的注解如下:

  • EnableCaching
  • Cacheable
  • CacheEvict
  • CachePut
  • CacheConfig
  • Caching

??目前版本包含了這六個注解,接下來我們將挨個簡單介紹下這些注解,然后提供一些簡單的例子來作為參考:

  1. 首先,這些注解都位于spring-context包下,具體地址是org.springframework.cache.annotation包下,我們通過API學習的時候可以根據這個地址進行查找。
  2. 其次,Spring Cache的XML實現,是通過命名空間cache來實現的,如果要基于XML來使用的話,記得先引入cache命名空間,接下來我們介紹各個注解的時候,會順便提一下對應的XML的配置。
2. EnableCaching注解

??該注解是最基礎的注解,用于啟用Spring的緩存管理功能,類似于Spring XML配置中的<cache:annotation-driven> 標簽。開啟Spring的緩存功能后,我們還需要配置CacheManager對象。
??CacheManager是用于管理spring cache的實現的,只要使用spring cache,該對象是必須要配置的。該接口有多種實現方式,比如 CaffeineCacheManager,EhCacheCacheManager,ConcurrentMapCacheManager,SimpleCacheManager等;我們來看一個簡單的配置:

@Configuration
@EnableCaching
public class AppConfig {
    @Bean
    public CacheManager cacheManager() {
        // configure and return an implementation of Spring's CacheManager SPI
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache("default")));
        return cacheManager;
    }
}

不過我們也可以繼承CachingConfigurerSupport來配置:

@Configuration
@EnableCaching
public class AppConfig extends CachingConfigurerSupport {

    @Bean
    @Override
    public CacheManager cacheManager() {
        // configure and return an implementation of Spring's CacheManager SPI
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache("default")));
        return cacheManager;
    }

    @Bean
    @Override
    public KeyGenerator keyGenerator() {
        // configure and return an implementation of Spring's KeyGenerator SPI
        return new MyKeyGenerator();
    }
}

而EnableCaching注解,有以下三個參數:

  1. proxyTargetClass,是否使用CGLIB代理,而不是默認的Java接口代理。默認是false;
  2. mode,緩存的代理模式,默認是AdviceMode.PROXY,有兩種選擇,另一種是AdviceMode.ASPECTJ。使用proxy代理的話,有一個問題就是,緩存方法在外部調用的時候才會被攔截生效,而同一個類的本地調用不會被攔截;這個問題適用于所有使用spring代理的情況,比如Transactional, Async等相關注解;并且proxy模式下,只有public方法上的才會生效;
  3. order,緩存的順序;

因為Spring Cache是基于Spring的代理模式來實現的,所以上面存在的內部調用問題還是要注意下。

3. Cacheable注解

該注解用于創建緩存,同樣來看一下它的參數。

  1. value/cacheNames,這兩個參數用于表示緩存的前綴,數組類型,可以指定多個;
  2. condition,SpEL 表達式,用于條件過濾,表示當滿足該條件的情況下才進行緩存;true或者false,只有為true時才進行緩存;
  3. key,SpEL表達式,緩存的key,Spring提供了專用的緩存相關元數據root對象和result:(redisKey = cacheNames::key (注意中間的兩個冒號))
元數據 對應的描述 示例
methodName 當前方法名 #root.methodName
method 當前方法 #root.method.name
target 當前被調用的對象 #root.target
targetClass 當前被調用的對象的class #root.targetClass
args 當前方法參數組成的數組 #root.args[0]
caches 當前被調用的方法使用的Cache #root.caches[0].name
參數名 方法參數的名字,可以直接 #參數名或者使用 #p0或#a0 的 形式, 0代表參數的索引; #iban, #a0, #p0, #p<#arg>
result 方法執行完成的返回值(僅當方法執行之后的判斷有效,如 unless, beforeInvocation=false #result

當然,我們在使用root對象的屬性作為key時我們也可以將“#root”省略,因為Spring默認使用的就是root對象的屬性。

其中key的生成策略有兩種,一種是默認策略,一種是自定義策略,但無論哪種策略,都是借助KeyGenerator生成的,其默認策略如下:

  • 1.如果方法沒有參數,則key是SimpleKey.EMPTY;
  • 2.如果只有一個參數的話則使用該參數作為key;
  • 3.如果參數多于一個的話,則返回一個包含所有參數的SimpleKey

The default key generation strategy changed with the release of Spring 4.0. Earlier versions of Spring used a key generation strategy that, for multiple key parameters, only considered the hashCode() of parameters and not equals(); this could cause unexpected key collisions (see SPR-10237 for background). The new 'SimpleKeyGenerator' uses a compound key for such scenarios.

大概意思是,原先版本默認的生成策略,對于多個參數的情況只考慮了參數的hashcode,而沒有考慮equals,這有可能導致意外的鍵沖突,而新的SimpleKeyGenerator則使用了符合鍵的操作。

如果默認策略滿足不了的話,我們可以自定義我們的KeyGenerator,然后指定Spring Cache使用的KeyGenerator為我們自己定義的KeyGenerator即可;

@Cacheable(value = "userCache", key = "#a0")
public User test(String id) {
    System.out.println("-------------------測試緩存方法-------------");
    User user = queryDb(id);
    return user;
}
  1. keyGenerator,前面已經說過,key的自定義生成器,如果不想使用key屬性,可以自定義key的實現,繼承KeyGenerator;該屬性和key是互斥的;
  2. cacheManager,cacheManager的bean配置;
  3. cacheResolver,同樣,如果不想使用默認的cacheManager的幾種實現,也可以通過該屬性來自定義實現,該屬性與cacheManager互斥;
  1. unless,用于否決緩存的,和condition不同,condition是在方法開始之前判斷,而該屬性是在方法執行完成之后判斷;所以condition不能通過方法返回值來判斷,而unless屬性可以;方法返回值是通過元數據result來表示的,true時表示不會緩存,為false時表示進行緩存;
@Cacheable(value = "userCache", unless = "#result == null")
public User test(String id) {
    System.out.println("-------------------測試緩存方法-------------");
    User user = queryDb(id);
    return user;
}

需要簡單注意的是,返回值如果是Optional的話,表示的是實際的對象,而不是包裝器對象;同樣,key的幾種表示方式也適合該屬性;

  1. sync,表示是否異步,默認為false,也就是同步;一般情況下,Spring的Cache的緩存過期之后,這時候如果多個線程同時對某個數據進行訪問,會同時去訪問數據庫,有可能導致數據庫的壓力頓時增大,所以Spring4.3之后引入了sync注解,當設置它為true時,會將緩存鎖定,只有一個線程的請求會去訪問數據庫,其他線程都會等待直到緩存可用,這個設置可以減少對數據庫的瞬間并發訪問;
    • 注意,不支持unless;實際上該屬性只是一個提示或建議,至于是否支持要看我們的cache provider ;
4. CacheEvict注解

??該注解用于緩存的清除,由于該注解的參數與Cacheable的參數大部分都是相同的,這里來簡單介紹下CacheEvict注解獨有的兩個參數:

  1. allEntries,是否清空所有的緩存內容,默認為false,如果指定為 true,則方法調用后將清空所有緩存;不過不允許在該值設置為true的情況下,再設置key的值;
  2. beforeInvocation,是否在調用該方法之前清空緩存,默認為false;如果為true,在該方法被調用前就清空緩存,不用考慮該方法的執行結果(即不考慮是否拋出異常);而默認情況下,如果方法執行時發生異常,則不會清除緩存;
@CacheEvict(value = "userCache", key = "#a0", allEntries = true)
public User evict(String id) {
    return queryDb(id);
}
5. CachePut注解

該注解用于緩存的更新,不過需要注意的是,如果返回值是JDK 8的Optional的類型的話程序會自動處理;參數方面和Cacheable完全一致,就不多說了。

6. CacheConfig注解

??用于類級別的緩存的公用配置。有的時候,一個類中多個緩存可能會有重復的屬性,我們可以使用該注解將這些重復的屬性提取出來。參數有cacheNameskeyGeneratorcacheManagercacheResolver這幾個屬性,我們前文已經說過,這里不多說了;

7. Caching注解

用于組合多個緩存的配置,是一個組注解;屬性直接看源碼就知道了:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Caching {

    Cacheable[] cacheable() default {};

    CachePut[] put() default {};

    CacheEvict[] evict() default {};

}};

使用的話可以看下:

@Caching(cacheable = {@Cacheable(value = "userCache", key = "#a0", unless = "#"),
      @Cacheable(value = "userCache", key = "#a0", unless = "#")})
            
8. 總結

??Spring Cache中涉及到的常用的注解就這幾個,其中多個注解的參數還都是相同的,所以學習起來比較簡單。

  1. 雖然這些注解可以用于方法和類上,但一般情況下都是用于方法上;
  2. 這些中的大部分注解都可以作為元注解,所以我們可以在這些注解基礎上自定義我們的注解;

三、Spring Cache的XML配置

雖然本篇文章的重點是介紹注解的,但還是簡單來說下XML中cache的配置。Spring的Cache的XML配置,最外層的標簽只有兩個:

1. <cache:annotation-driven>

該標簽我們前文簡單說過,用于開啟Spring的緩存功能,其中該標簽的幾個屬性和EnableCaching注解的幾個參數是相同的:

  1. modeproxy-target-classorder,這三個參數EnableCaching注解是相同的;
  2. cache-manager,這個我們也說過了,用于配置緩存的實現CacheManager,key-generator,這個則是自定義key的實現;
<cache:annotation-driven key-generator="" cache-manager="" order="" proxy-target-class="" 
    mode="proxy"/>
2. <cache:advice>

該標簽則是用于配置緩存的具體實現,包含創建,清除,更新等對應功能。我們先來看下配置:

<cache:advice cache-manager="" key-generator="" id="">
    <cache:caching key-generator="" cache-manager="" condition="" cache="" key="" method="">
        <cache:cacheable method="" key="" cache="" condition="" cache-manager="" key-generator="" unless=""/>
        <cache:cache-put method="" key="" cache="" condition="" cache-manager="" key-generator="" unless=""/>
        <cache:cache-evict method="" key="" cache="" condition="" cache-manager=""  key-generator=""  before-invocation="true" all-entries="true"/>
    </cache:caching>
</cache:advice>

<cache:advice>的所有配置都在這了,而這些配置屬性與注解中的屬性是一一對應的,這里就不多說了。

四、Spring Cache 的一些小問題

1. 緩存有效時間問題

??前面學習@Cacheable的使用的時候,沒有涉及到有關緩存的有效時間的設置。這是因為Spring Cache自帶的默認的基于ConcurrentHashMap的CacheManager實現是沒有自動過期這一功能的。Spring Cache支持了許多第三方的Cache實現,不同的Cache對過期時間的處理是不一樣的,所以我們如果需要實現有效時間的問題,可以采用如下幾種方式:

  1. 以@Cachable為元注解,自定義我們的的注解實現;
  2. 在@CacheEvict基礎上,結合Scheduled注解,通過定時任務的形式來實現,不過要注意多個key的問題;
  3. 使用第三方Cache,如Redis,EhCache等實現;

比如說:

public CacheManager cacheManager() {
    RedisCacheManager redisCacheManager =newRedisCacheManager(redisTemplate());
    redisCacheManager.setTransactionAware(true);
    redisCacheManager.setLoadRemoteCachesOnStartup(true);
    redisCacheManager.setUsePrefix(true);
    //配置緩存的過期時間
    Mapexpires =newHashMap();
    expires.put("token",expiration);
    redisCacheManager.setExpires(expires);
    return redisCacheManager;
}

這里截取Spring Cache文檔最后一節:

36.8 How can I set the TTL/TTI/Eviction policy/XXX feature?
Directly through your cache provider. The cache abstraction is… well, an abstraction not a cache implementation. The solution you are using might support various data policies and different topologies which other solutions do not (take for example the JDK ConcurrentHashMap) - exposing that in the cache abstraction would be useless simply because there would no backing support. Such functionality should be controlled directly through the backing cache, when configuring it or through its native API.

本文參考自:
Spring 4.3.15 官方API 文檔Spring 4.3.15 官方文檔(強烈建議查看)
IBM社區-注釋驅動的 Spring cache 緩存介紹
使用Spring4.3解決緩存過期后多線程并發訪問數據庫的問題

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374