spring cache的使用
緩存某些方法的執(zhí)行結(jié)果
設(shè)置好緩存配置之后我們就可以使用 @Cacheable 注解來(lái)緩存方法執(zhí)行的結(jié)果了
spring cache的使用是非常簡(jiǎn)單的,只需要在方法上標(biāo)注 @Cacheable 注解就可以
@Cacheable("getName")
public String getName(String id) {
return xxx;
}
@Cacheable("getUser")
public User searchCity(String userId){
return this.query(userId);
}
整個(gè)@Cacheable的流程是:先從緩存中讀取,如果沒(méi)有再調(diào)用方法獲取數(shù)據(jù),然后把數(shù)據(jù)添加到緩存中.
緩存數(shù)據(jù)一致性保證
在我們配置了查詢用戶名的方法緩存之后,假如這時(shí)候我們改名了,我想讓我自己查詢的時(shí)候能看到我自己的改變。這時(shí)候我們?cè)撛趺醋瞿兀?/p>
@CacheEvict移除緩存
我們這時(shí)候可以在修改方法上加上@CacheEvict注解,來(lái)更新查詢方法的結(jié)果緩存
@CacheEvict(value = "getName", key = "#user.id") //移除指定key的數(shù)據(jù)
public User updateUser(User user) {
users.update(user);
return user;
}
這時(shí)候再調(diào)用新的查詢接口,我們就不會(huì)走緩存里的方法,會(huì)重新去數(shù)據(jù)庫(kù)中查詢.
因?yàn)檎{(diào)用修改用戶的方法的時(shí)候, @CacheEvict會(huì)根據(jù)相應(yīng)的key和value作為條件從緩存中移除相應(yīng)的數(shù)據(jù).
@CachePut 既要保證方法被調(diào)用,又希望結(jié)果被緩存
據(jù)前面的例子,我們知道,如果使用了 @Cacheable 注釋,則當(dāng)重復(fù)使用相同參數(shù)調(diào)用方法的時(shí)候,方法本身不會(huì)被調(diào)用執(zhí)行,即方法本身被略過(guò)了,取而代之的是方法的結(jié)果直接從緩存中找到并返回了.
現(xiàn)實(shí)中并不總是如此,有些情況下我們希望方法一定會(huì)被調(diào)用,因?yàn)槠涑朔祷匾粋€(gè)結(jié)果,還做了其他事情,例如記錄日志,調(diào)用接口等,這個(gè)時(shí)候,我們可以用 @CachePut 注釋,這個(gè)注釋可以確保方法被執(zhí)行,同時(shí)方法的返回值也被記錄到緩存中.
@Cacheable(value="userCache")
public User getUserById(String id) {
// 方法內(nèi)部實(shí)現(xiàn)不考慮緩存邏輯,直接實(shí)現(xiàn)業(yè)務(wù)
return getUserFromDB(id);
}
// 更新 userCache 緩存
@CachePut(value="userCache",key="#user.id")
public User updateUser(User user) {
return updateUserToDB(user);
}
private User updateUserToDB(User user) {
// do some query
return user;
}
如上面的代碼所示,我們首先用 getUserById 方法查詢一個(gè)人的信息,這個(gè)時(shí)候會(huì)查詢數(shù)據(jù)庫(kù)一次,但是也記錄到緩存中了。然后我們修改了用戶信息,調(diào)用了 updateUser 方法,這個(gè)時(shí)候會(huì)執(zhí)行數(shù)據(jù)庫(kù)的更新操作且記錄到緩存,我們?cè)俅涡薷男畔⒉⒄{(diào)用 updateUser 方法,然后通過(guò) getUserById 方法查詢,這個(gè)時(shí)候,由于緩存中已經(jīng)有數(shù)據(jù),所以不會(huì)查詢數(shù)據(jù)庫(kù),而是直接返回最新的數(shù)據(jù)
三種方式的對(duì)比
@Cacheable、@CachePut、@CacheEvict 注釋介紹
- @Cacheable 主要針對(duì)方法配置,能夠根據(jù)方法的請(qǐng)求參數(shù)對(duì)其結(jié)果進(jìn)行緩存
- @CachePut 主要針對(duì)方法配置,能夠根據(jù)方法的請(qǐng)求參數(shù)對(duì)其結(jié)果進(jìn)行緩存,和 @Cacheable 不同的是,它每次都會(huì)觸發(fā)真實(shí)方法的調(diào)用
- @CachEvict 主要針對(duì)方法配置,能夠根據(jù)一定的條件對(duì)緩存進(jìn)行清空
基本原理
一句話介紹就是Spring AOP的動(dòng)態(tài)代理技術(shù)。 如果讀者對(duì)Spring AOP不熟悉的話,可以去看看官方文檔
注意和限制
基于 proxy 的 spring aop 帶來(lái)的內(nèi)部調(diào)用問(wèn)題
上面介紹過(guò) spring cache 的原理,即它是基于動(dòng)態(tài)生成的 proxy 代理機(jī)制來(lái)對(duì)方法的調(diào)用進(jìn)行切面,這里關(guān)鍵點(diǎn)是對(duì)象的引用問(wèn)題.
如果對(duì)象的方法是內(nèi)部調(diào)用(即 this 引用)而不是外部引用,則會(huì)導(dǎo)致 proxy 失效,那么我們的切面就失效,也就是說(shuō)上面定義的各種注釋包括 @Cacheable、@CachePut 和 @CacheEvict 都會(huì)失效,我們來(lái)演示一下。
public Account getAccountByName2(String accountName) {
return this.getAccountByName(accountName);
}
@Cacheable(value="accountCache")// 使用了一個(gè)緩存名叫 accountCache
public Account getAccountByName(String accountName) {
// 方法內(nèi)部實(shí)現(xiàn)不考慮緩存邏輯,直接實(shí)現(xiàn)業(yè)務(wù)
return getFromDB(accountName);
}
上面我們定義了一個(gè)新的方法 getAccountByName2,其自身調(diào)用了 getAccountByName 方法,這個(gè)時(shí)候,發(fā)生的是內(nèi)部調(diào)用(this),所以沒(méi)有走 proxy,導(dǎo)致 spring cache 失效
要避免這個(gè)問(wèn)題,就是要避免對(duì)緩存方法的內(nèi)部調(diào)用,或者避免使用基于 proxy 的 AOP 模式,可以使用基于 aspectJ 的 AOP 模式來(lái)解決這個(gè)問(wèn)題。
@CacheEvict 的可靠性問(wèn)題
我們看到,@CacheEvict 注釋有一個(gè)屬性 beforeInvocation,缺省為 false,即缺省情況下,都是在實(shí)際的方法執(zhí)行完成后,才對(duì)緩存進(jìn)行清空操作。期間如果執(zhí)行方法出現(xiàn)異常,則會(huì)導(dǎo)致緩存清空不被執(zhí)行。我們演示一下
// 清空 accountCache 緩存
@CacheEvict(value="accountCache",allEntries=true)
public void reload() {
throw new RuntimeException();
}
我們的測(cè)試代碼如下:
accountService.getAccountByName("someone");
accountService.getAccountByName("someone");
try {
accountService.reload();
} catch (Exception e) {
//...
}
accountService.getAccountByName("someone");
注意上面的代碼,我們?cè)?reload 的時(shí)候拋出了運(yùn)行期異常,這會(huì)導(dǎo)致清空緩存失敗。上面的測(cè)試代碼先查詢了兩次,然后 reload,然后再查詢一次,結(jié)果應(yīng)該是只有第一次查詢走了數(shù)據(jù)庫(kù),其他兩次查詢都從緩存,第三次也走緩存因?yàn)?reload 失敗了。
那么我們?nèi)绾伪苊膺@個(gè)問(wèn)題呢?我們可以用 @CacheEvict 注釋提供的 beforeInvocation 屬性,將其設(shè)置為 true,這樣,在方法執(zhí)行前我們的緩存就被清空了。可以確保緩存被清空。
非 public 方法問(wèn)題
和內(nèi)部調(diào)用問(wèn)題類似,非 public 方法如果想實(shí)現(xiàn)基于注釋的緩存,必須采用基于 AspectJ 的 AOP 機(jī)制
Dummy CacheManager 的配置和作用
有的時(shí)候,我們?cè)诖a遷移、調(diào)試或者部署的時(shí)候,恰好沒(méi)有 cache 容器,比如 memcache 還不具備條件,h2db 還沒(méi)有裝好等,如果這個(gè)時(shí)候你想調(diào)試代碼,豈不是要瘋掉?這里有一個(gè)辦法,在不具備緩存條件的時(shí)候,在不改代碼的情況下,禁用緩存。
方法就是修改 spring的配置文件,設(shè)置一個(gè)找不到緩存就不做任何操作的標(biāo)志位
<cache:annotation-driven />
<bean id="simpleCacheManager" class="org.springframework.cache.support.SimpleCacheManager">
<property name="caches">
<set>
<bean
class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"
p:name="default" />
</set>
</property>
</bean>
<bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager">
<property name="cacheManagers">
<list>
<ref bean="simpleCacheManager" />
</list>
</property>
<property name="fallbackToNoOpCache" value="true" />
</bean>
注意以前的 cacheManager 變?yōu)榱?simpleCacheManager,且沒(méi)有配置 accountCache 實(shí)例,后面的 cacheManager 的實(shí)例是一個(gè) CompositeCacheManager,他利用了前面的 simpleCacheManager 進(jìn)行查詢,如果查詢不到,則根據(jù)標(biāo)志位 fallbackToNoOpCache 來(lái)判斷是否不做任何緩存操作。
使用 guava cache
<bean id="cacheManager" class="org.springframework.cache.guava.GuavaCacheManager">
<property name="cacheSpecification" value="concurrencyLevel=4,expireAfterAccess=100s,expireAfterWrite=100s" />
<property name="cacheNames">
<list>
<value>dictTableCache</value>
</list>
</property>
</bean>
代碼地址
https://github.com/rollenholt/spring-cache-example
參考鏈接
csdn的技術(shù)文:Redis 緩存 + Spring 的集成示例
Ibm的技術(shù)文:注釋驅(qū)動(dòng)的 Spring cache 緩存介紹
開(kāi)濤大神的技術(shù)博客:Spring Cache抽象詳解