[spring]Spring Cache學(xué)習(xí)

一、前言

??最近在和同事一塊做一個(gè)需求的時(shí)候,看了下同事的代碼湘纵,在使用緩存這塊看到了Spring Cache這個(gè)玩意塑崖,感到很陌生。看了下簡介,這玩意從Spring 3.X之后都有了,但我卻一點(diǎn)也不了解妥箕,感到很慚愧,于是趕緊花了點(diǎn)時(shí)間學(xué)習(xí)下更舞。

本文已學(xué)習(xí)注解為主畦幢,畢竟Spring Boot這類注解式的框架已逐漸成為了主流。參考的Spring版本是4.3.15缆蝉。

二宇葱、Spring Cache注解

1. 簡介

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

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

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

  1. 首先印颤,這些注解都位于spring-context包下,具體地址是org.springframework.cache.annotation包下穿肄,我們通過API學(xué)習(xí)的時(shí)候可以根據(jù)這個(gè)地址進(jìn)行查找年局。
  2. 其次,Spring Cache的XML實(shí)現(xiàn)被碗,是通過命名空間cache來實(shí)現(xiàn)的某宪,如果要基于XML來使用的話仿村,記得先引入cache命名空間锐朴,接下來我們介紹各個(gè)注解的時(shí)候,會順便提一下對應(yīng)的XML的配置蔼囊。
2. EnableCaching注解

??該注解是最基礎(chǔ)的注解焚志,用于啟用Spring的緩存管理功能,類似于Spring XML配置中的<cache:annotation-driven> 標(biāo)簽畏鼓。開啟Spring的緩存功能后酱酬,我們還需要配置CacheManager對象。
??CacheManager是用于管理spring cache的實(shí)現(xiàn)的云矫,只要使用spring cache膳沽,該對象是必須要配置的。該接口有多種實(shí)現(xiàn)方式,比如 CaffeineCacheManager挑社,EhCacheCacheManager陨界,ConcurrentMapCacheManager,SimpleCacheManager等痛阻;我們來看一個(gè)簡單的配置:

@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注解菌瘪,有以下三個(gè)參數(shù):

  1. proxyTargetClass,是否使用CGLIB代理阱当,而不是默認(rèn)的Java接口代理俏扩。默認(rèn)是false;
  2. mode弊添,緩存的代理模式录淡,默認(rèn)是AdviceMode.PROXY,有兩種選擇表箭,另一種是AdviceMode.ASPECTJ赁咙。使用proxy代理的話,有一個(gè)問題就是免钻,緩存方法在外部調(diào)用的時(shí)候才會被攔截生效彼水,而同一個(gè)類的本地調(diào)用不會被攔截;這個(gè)問題適用于所有使用spring代理的情況极舔,比如Transactional, Async等相關(guān)注解凤覆;并且proxy模式下,只有public方法上的才會生效拆魏;
  3. order盯桦,緩存的順序;

因?yàn)镾pring Cache是基于Spring的代理模式來實(shí)現(xiàn)的渤刃,所以上面存在的內(nèi)部調(diào)用問題還是要注意下拥峦。

3. Cacheable注解

該注解用于創(chuàng)建緩存,同樣來看一下它的參數(shù)卖子。

  1. value/cacheNames略号,這兩個(gè)參數(shù)用于表示緩存的前綴,數(shù)組類型洋闽,可以指定多個(gè)玄柠;
  2. condition,SpEL 表達(dá)式诫舅,用于條件過濾羽利,表示當(dāng)滿足該條件的情況下才進(jìn)行緩存;true或者false刊懈,只有為true時(shí)才進(jìn)行緩存这弧;
  3. key娃闲,SpEL表達(dá)式,緩存的key匾浪,Spring提供了專用的緩存相關(guān)元數(shù)據(jù)root對象和result:(redisKey = cacheNames::key (注意中間的兩個(gè)冒號))
元數(shù)據(jù) 對應(yīng)的描述 示例
methodName 當(dāng)前方法名 #root.methodName
method 當(dāng)前方法 #root.method.name
target 當(dāng)前被調(diào)用的對象 #root.target
targetClass 當(dāng)前被調(diào)用的對象的class #root.targetClass
args 當(dāng)前方法參數(shù)組成的數(shù)組 #root.args[0]
caches 當(dāng)前被調(diào)用的方法使用的Cache #root.caches[0].name
參數(shù)名 方法參數(shù)的名字畜吊,可以直接 #參數(shù)名或者使用 #p0或#a0 的 形式, 0代表參數(shù)的索引户矢; #iban, #a0, #p0, #p<#arg>
result 方法執(zhí)行完成的返回值(僅當(dāng)方法執(zhí)行之后的判斷有效玲献,如 unless, beforeInvocation=false #result

當(dāng)然,我們在使用root對象的屬性作為key時(shí)我們也可以將“#root”省略梯浪,因?yàn)镾pring默認(rèn)使用的就是root對象的屬性捌年。

其中key的生成策略有兩種,一種是默認(rèn)策略挂洛,一種是自定義策略礼预,但無論哪種策略,都是借助KeyGenerator生成的虏劲,其默認(rèn)策略如下:

  • 1.如果方法沒有參數(shù)托酸,則key是SimpleKey.EMPTY;
  • 2.如果只有一個(gè)參數(shù)的話則使用該參數(shù)作為key;
  • 3.如果參數(shù)多于一個(gè)的話,則返回一個(gè)包含所有參數(shù)的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.

大概意思是励堡,原先版本默認(rèn)的生成策略,對于多個(gè)參數(shù)的情況只考慮了參數(shù)的hashcode堡掏,而沒有考慮equals应结,這有可能導(dǎo)致意外的鍵沖突,而新的SimpleKeyGenerator則使用了符合鍵的操作泉唁。

如果默認(rèn)策略滿足不了的話鹅龄,我們可以自定義我們的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扮休,前面已經(jīng)說過,key的自定義生成器拴鸵,如果不想使用key屬性玷坠,可以自定義key的實(shí)現(xiàn),繼承KeyGenerator宝踪;該屬性和key是互斥的侨糟;
  2. cacheManager碍扔,cacheManager的bean配置瘩燥;
  3. cacheResolver,同樣不同,如果不想使用默認(rèn)的cacheManager的幾種實(shí)現(xiàn)厉膀,也可以通過該屬性來自定義實(shí)現(xiàn)溶耘,該屬性與cacheManager互斥;
  1. unless服鹅,用于否決緩存的凳兵,和condition不同,condition是在方法開始之前判斷企软,而該屬性是在方法執(zhí)行完成之后判斷庐扫;所以condition不能通過方法返回值來判斷,而unless屬性可以仗哨;方法返回值是通過元數(shù)據(jù)result來表示的形庭,true時(shí)表示不會緩存,為false時(shí)表示進(jìn)行緩存厌漂;
@Cacheable(value = "userCache", unless = "#result == null")
public User test(String id) {
    System.out.println("-------------------測試緩存方法-------------");
    User user = queryDb(id);
    return user;
}

需要簡單注意的是萨醒,返回值如果是Optional的話,表示的是實(shí)際的對象苇倡,而不是包裝器對象富纸;同樣,key的幾種表示方式也適合該屬性旨椒;

  1. sync晓褪,表示是否異步,默認(rèn)為false综慎,也就是同步辞州;一般情況下,Spring的Cache的緩存過期之后寥粹,這時(shí)候如果多個(gè)線程同時(shí)對某個(gè)數(shù)據(jù)進(jìn)行訪問变过,會同時(shí)去訪問數(shù)據(jù)庫,有可能導(dǎo)致數(shù)據(jù)庫的壓力頓時(shí)增大涝涤,所以Spring4.3之后引入了sync注解媚狰,當(dāng)設(shè)置它為true時(shí),會將緩存鎖定阔拳,只有一個(gè)線程的請求會去訪問數(shù)據(jù)庫崭孤,其他線程都會等待直到緩存可用,這個(gè)設(shè)置可以減少對數(shù)據(jù)庫的瞬間并發(fā)訪問糊肠;
    • 注意辨宠,不支持unless;實(shí)際上該屬性只是一個(gè)提示或建議货裹,至于是否支持要看我們的cache provider 嗤形;
4. CacheEvict注解

??該注解用于緩存的清除,由于該注解的參數(shù)與Cacheable的參數(shù)大部分都是相同的弧圆,這里來簡單介紹下CacheEvict注解獨(dú)有的兩個(gè)參數(shù):

  1. allEntries赋兵,是否清空所有的緩存內(nèi)容笔咽,默認(rèn)為false,如果指定為 true霹期,則方法調(diào)用后將清空所有緩存叶组;不過不允許在該值設(shè)置為true的情況下,再設(shè)置key的值历造;
  2. beforeInvocation甩十,是否在調(diào)用該方法之前清空緩存,默認(rèn)為false吭产;如果為true枣氧,在該方法被調(diào)用前就清空緩存,不用考慮該方法的執(zhí)行結(jié)果(即不考慮是否拋出異常)垮刹;而默認(rèn)情況下达吞,如果方法執(zhí)行時(shí)發(fā)生異常,則不會清除緩存荒典;
@CacheEvict(value = "userCache", key = "#a0", allEntries = true)
public User evict(String id) {
    return queryDb(id);
}
5. CachePut注解

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

6. CacheConfig注解

??用于類級別的緩存的公用配置遮咖。有的時(shí)候滩字,一個(gè)類中多個(gè)緩存可能會有重復(fù)的屬性,我們可以使用該注解將這些重復(fù)的屬性提取出來御吞。參數(shù)有cacheNames麦箍、keyGeneratorcacheManager陶珠、cacheResolver這幾個(gè)屬性挟裂,我們前文已經(jīng)說過,這里不多說了揍诽;

7. Caching注解

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

@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. 總結(jié)

??Spring Cache中涉及到的常用的注解就這幾個(gè)暑脆,其中多個(gè)注解的參數(shù)還都是相同的渠啤,所以學(xué)習(xí)起來比較簡單。

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

三架专、Spring Cache的XML配置

雖然本篇文章的重點(diǎn)是介紹注解的,但還是簡單來說下XML中cache的配置玄帕。Spring的Cache的XML配置部脚,最外層的標(biāo)簽只有兩個(gè):

1. <cache:annotation-driven>

該標(biāo)簽我們前文簡單說過,用于開啟Spring的緩存功能裤纹,其中該標(biāo)簽的幾個(gè)屬性和EnableCaching注解的幾個(gè)參數(shù)是相同的:

  1. mode委刘,proxy-target-classorder鹰椒,這三個(gè)參數(shù)EnableCaching注解是相同的锡移;
  2. cache-manager,這個(gè)我們也說過了漆际,用于配置緩存的實(shí)現(xiàn)CacheManager淆珊,key-generator,這個(gè)則是自定義key的實(shí)現(xiàn)奸汇;
<cache:annotation-driven key-generator="" cache-manager="" order="" proxy-target-class="" 
    mode="proxy"/>
2. <cache:advice>

該標(biāo)簽則是用于配置緩存的具體實(shí)現(xiàn)施符,包含創(chuàng)建,清除擂找,更新等對應(yīng)功能戳吝。我們先來看下配置:

<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>的所有配置都在這了,而這些配置屬性與注解中的屬性是一一對應(yīng)的贯涎,這里就不多說了听哭。

四、Spring Cache 的一些小問題

1. 緩存有效時(shí)間問題

??前面學(xué)習(xí)@Cacheable的使用的時(shí)候塘雳,沒有涉及到有關(guān)緩存的有效時(shí)間的設(shè)置陆盘。這是因?yàn)镾pring Cache自帶的默認(rèn)的基于ConcurrentHashMap的CacheManager實(shí)現(xiàn)是沒有自動過期這一功能的。Spring Cache支持了許多第三方的Cache實(shí)現(xiàn)败明,不同的Cache對過期時(shí)間的處理是不一樣的礁遣,所以我們?nèi)绻枰獙?shí)現(xiàn)有效時(shí)間的問題,可以采用如下幾種方式:

  1. 以@Cachable為元注解肩刃,自定義我們的的注解實(shí)現(xiàn)祟霍;
  2. 在@CacheEvict基礎(chǔ)上,結(jié)合Scheduled注解盈包,通過定時(shí)任務(wù)的形式來實(shí)現(xiàn)沸呐,不過要注意多個(gè)key的問題;
  3. 使用第三方Cache呢燥,如Redis崭添,EhCache等實(shí)現(xiàn);

比如說:

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

這里截取Spring Cache文檔最后一節(jié):

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 官方文檔(強(qiáng)烈建議查看)
IBM社區(qū)-注釋驅(qū)動的 Spring cache 緩存介紹
使用Spring4.3解決緩存過期后多線程并發(fā)訪問數(shù)據(jù)庫的問題

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末呼渣,一起剝皮案震驚了整個(gè)濱河市棘伴,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌屁置,老刑警劉巖焊夸,帶你破解...
    沈念sama閱讀 219,110評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蓝角,居然都是意外死亡阱穗,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評論 3 395
  • 文/潘曉璐 我一進(jìn)店門使鹅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來揪阶,“玉大人,你說我怎么就攤上這事患朱÷沉牛” “怎么了?”我有些...
    開封第一講書人閱讀 165,474評論 0 356
  • 文/不壞的土叔 我叫張陵裁厅,是天一觀的道長蕴茴。 經(jīng)常有香客問我,道長姐直,這世上最難降的妖魔是什么倦淀? 我笑而不...
    開封第一講書人閱讀 58,881評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮声畏,結(jié)果婚禮上撞叽,老公的妹妹穿的比我還像新娘。我一直安慰自己插龄,他們只是感情好愿棋,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,902評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著均牢,像睡著了一般糠雨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上徘跪,一...
    開封第一講書人閱讀 51,698評論 1 305
  • 那天甘邀,我揣著相機(jī)與錄音,去河邊找鬼垮庐。 笑死松邪,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的哨查。 我是一名探鬼主播逗抑,決...
    沈念sama閱讀 40,418評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了邮府?” 一聲冷哼從身側(cè)響起荧关,我...
    開封第一講書人閱讀 39,332評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎褂傀,沒想到半個(gè)月后忍啤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,796評論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡紊服,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,968評論 3 337
  • 正文 我和宋清朗相戀三年檀轨,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了胸竞。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片欺嗤。...
    茶點(diǎn)故事閱讀 40,110評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖卫枝,靈堂內(nèi)的尸體忽然破棺而出煎饼,到底是詐尸還是另有隱情,我是刑警寧澤校赤,帶...
    沈念sama閱讀 35,792評論 5 346
  • 正文 年R本政府宣布吆玖,位于F島的核電站,受9級特大地震影響马篮,放射性物質(zhì)發(fā)生泄漏沾乘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,455評論 3 331
  • 文/蒙蒙 一浑测、第九天 我趴在偏房一處隱蔽的房頂上張望翅阵。 院中可真熱鬧,春花似錦迁央、人聲如沸掷匠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽讹语。三九已至,卻和暖如春蜂科,著一層夾襖步出監(jiān)牢的瞬間顽决,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評論 1 272
  • 我被黑心中介騙來泰國打工导匣, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留擎值,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,348評論 3 373
  • 正文 我出身青樓逐抑,卻偏偏與公主長得像鸠儿,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,047評論 2 355