摘要: 本文講的是java中利用spring cache解耦業務中的緩衝, 雖然以前實現緩衝的方式,是定義了快取作業介面,可以靈活實現不同的緩衝,可畢竟精力有限,要完成不同的緩衝實現也是件麻煩的事。更要命的是,業務代碼中有大量快取作業的代碼,耦合度太高,看著很不優雅。 所以呢,抽空瞭解了一下其它實現方案。 雲端運算 Elastic Compute Service 大資料 建站 備案 文檔 網域名稱 whois查詢 文集
雖然以前實現緩衝的方式,是定義了快取作業介面,可以靈活實現不同的緩衝,可畢竟精力有限,要完成不同的緩衝實現也是件麻煩的事。更要命的是,業務代碼中有大量快取作業的代碼,耦合度太高,看著很不優雅。
所以呢,抽空瞭解了一下其它實現方案。這不,spring3.1開始,支援基於註解的緩衝,算是目前我比較可以接受的一種方案吧。學完之後還是做一下筆記吧。
spring cache是一套基於註解實現的緩衝技術,其本身是並不是具體實現,不過預設實現了ConcurrentMap和EHCache實現的緩衝。當然也是支援其它緩衝的。
spring cache有哪些特性:
1.通過少量的配置 annotation 註解即可使得既有代碼支援緩衝 (非常節省開發時間)
2.支援開箱即用 Out-Of-The-Box,即不用安裝和部署額外第三方組件即可使用緩衝(位於spring-context包中,spring web項目都會引用這個包)
3.支援 Spring Express Language,能使用對象的任何屬性或者方法來定義緩衝的 key 和 condition (支援SpEL文法)
4.支援 AspectJ,並通過其實現任何方法的緩衝支援 (預設基於AOP方案,採用AspectJ會更靈活,下文有介紹)
5.支援自訂 key 和自訂緩衝管理者,具有相當的靈活性和擴充性(如果SpEL達不到你的預期,勇敢實現自己的KeyGenerator吧)
6.支援各種緩衝實現,預設是基於ConcurrentMap實現的ConcurrentMapCache,同時支援ehcache實現。若要使用redis等緩衝,引入redis的實現包即可。
有哪些遺憾呢。在我考慮的情境下,還有下面的一些遺憾:
1.不支援TTL,也就是不能設定expires time。這點挺遺憾的,spring-cache認為這是各個cache實現自己去完成的事情,有方案但是只能設定統一的到期時間,這明顯不夠靈活。比如使用者的抽獎次數、有效期間等業務,當天有效,或者3天、一周有效,我們傾向於設定緩衝有效期間解決這個問題,而spring-cache卻無法完成。
2.無法根據查詢結果中的內容產生緩衝key,比如getUser(uid)方法,想通過查詢出來的user.email產生緩衝key就無法實現了。
3.調試起來麻煩
先貼一段以前的代碼,看看以前是怎麼做的:
/**
* 先取cache。如果沒有,從DB取,再存cache
*/
@Override
public UserDetail getUserDetail(int userId) {
Result<String> info = cacheManager.get(UCConstants.nameSpace,
UCUtil.getKey(UCConstants.USER_DETAIL_UID_KEY, userId));
if (StringUtils.isNotEmpty(info.getEntity())) {
UserDetail userDetail = JSONUtils.fromJSON(info.getEntity(), UserDetail.class);
if (null != userDetail) {
return userDetail;
}
}
UserDetail userDetail = userDetailDAO.getUserDetail(userId);
if (userDetail != null) {
if (logger.isDebugEnabled()) {
logger.info("getUserDetail from db userDetail=" + userDetail.toString());
}
addToCache(userDetail);
return userDetail;
}
return null;
}
private void addToCache(UserDetail userDetail) {
cacheManager.put(UCConstants.nameSpace,
UCUtil.getKey(UCConstants.USER_DETAIL_UID_KEY, userDetail.getId()),
JSONUtils.toJSON(userDetail), UCConstants.USER_DETAIL_CACHE_TIME);
//節約空間,存id吧。多查一次吧
cacheManager.put(UCConstants.nameSpace,
UCUtil.getKey(UCConstants.USER_DETAIL_NAME_KEY, userDetail.getUserNickName()),
userDetail.getId() + "", UCConstants.USER_DETAIL_CACHE_TIME);
}
那採用註解驅動的spring-cache之後代碼怎麼寫的:
@Cacheable(key="#uid", value = "userCache")
public UserDetail getUserDetail(int uid){
return userDetailMapper.getUser(uid);
}
當然,以前的代碼寫了兩個緩衝key,這點通過@Caching註解也是可以實現的,樣本如下:
@Caching
(evict={@CacheEvict(value="userCache",key="#user.uid"),
@CacheEvict(value="userCache",key="#user.email")})
看了以上的代碼,覺得用註解驅動的cache是不是很過癮。代碼簡介,低耦合。簡直是廣大程式猿的福音。要像上面那樣寫代碼,其實也挺容易的。下面就從頭開始吧。
來,先把配置弄起。spring這點就很不爽,什麼都要弄個配置。不過也正是基於配置+註解的方式,使得我們脫離了不斷去new對象的苦海。
我的Cache是不打算使用ConcurrentMapCache的,所以我就直接拿EHCache來做樣本好了。在使用EHCache之前,我們需要引入ehcache的包,以及配置ehcache.xml檔案。
當然,線上上生產環境中,還是不建議只用ehcache這種方式。可以採用ehcache+redis,或者redis的方案都可以。
Maven的pom.xml檔案配置:
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.6.9</version>
</dependency>
ehcache的配置,位於classpath下的ehcache.xml檔案:
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd" updateCheck="false">
<diskStore path="D:/cache" /> <!-- 緩衝存放目錄(此目錄為放入系統預設緩衝目錄),也可以是”D:/cache“ java.io.tmpdir -->
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
maxElementsOnDisk="10000000"
diskPersistent="true"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"
/>
<cache name="userCache"
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
maxElementsOnDisk="10000000"
diskPersistent="true"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"
/>
</ehcache>
這裡說明一下,為什麼兩個cache呢,用一個不好嗎。
我最早的時候是一個defaultCache,沒有userCache。後來發現spring-cache配置的EhCacheCacheManager總是load失敗,報錯誤:
loadCaches must not return an empty Collection
我很納悶,這不是有一個defaultCache嗎。於是我跟蹤代碼,在AbstractCacheManager源碼的afterPropertiesSet方法中有如下代碼:
public void afterPropertiesSet() {
Collection<? extends Cache> caches = loadCaches();
Assert.notEmpty(caches, "loadCaches must not return an empty Collection");
this.cacheMap.clear();
// preserve the initial order of the cache names
for (Cache cache : caches) {
this.cacheMap.put(cache.getName(), cache);
this.cacheNames.add(cache.getName());
}
}
這裡取到的caches居然為empty。確實我也不知道為什麼會這樣。我當時嘗試增加了一個名為default的cache配置,結果ehcache又報錯,提示已經有名為default的cache了。於是將name改為userCache,問題解決。
從ehcache的報錯來看,ehcache應該配置了一個名為default的cache,但不知道為什麼spring-cache認不到。知道的同學可以告訴下我。
配置好ehcache後,就該配置我們的spring-cache了,為了方便管理,我傾向於配置獨立的spring-cache.xml檔案放在spring的配置目錄下。內容如下:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:cache="http://www.springframework.org/schema/cache"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/cache
http://www.springframework.org/schema/cache/spring-cache.xsd">
<cache:annotation-driven cache-manager="ehCacheManager"/>
<!-- 緩衝 屬性-->
<bean id="ehCacheManagerFactory" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<property name="configLocation" value="classpath:ehcache.xml"/>
</bean>
<!-- generic cache manager -->
<bean id="ehCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
<property name="cacheManager" ref="ehCacheManagerFactory"/>
</bean>
</beans>
當然,如果你想在沒有緩衝的環境中不做任何代碼上的修改(比如環境遷移、臨時測試等),即可簡單的切換,那也是OK的。
又或者,你的環境中既有ehcache,又有redis,還有ConcurrentMapCache,那也是可以的。
上面說到的亮點,你可以用CompositeCacheManager完成。
需要重新設定以下的cacheManager:
<bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager">
<property name="cacheManagers">
<list>
<ref bean="ehCacheManager"/>
<ref bean="otherCachaManager"/>
</list>
</property>
<property name="fallbackToNoOpCache" value="true"/>
</bean>
fallbackToNoOpCache參數決定了在沒有Cachhe的情況下會出現什麼現象。如果為true,則會直接忽略掉緩衝,可能進入db查詢;如果為false(預設為false),則在無緩衝時會拋出異常:
Cannot find cache named [userCache] for CacheableOperation
配置好了,那麼該幹正事了。spring-cache的使用非常簡單,只會用簡單的幾個註解即可。那麼,有哪些註解呢。看看下錶:
Spring Cache配置 |
JSR-107規範 |
描述 |
@Cacheable |
@CacheResult |
緩衝方法返回的結果,有三個參數,分別是value(緩衝名稱)、key(緩衝key)、condition(緩衝條件) |
@CachePut |
@CachePut |
緩衝方法返回的結果,並且會在方法被調用的時候執行。參數同Cacheable |
@CacheEvict |
@CacheRemove |
清除緩衝。有五個參數,除過上面的3個外,還有2個:allEntries(是否清除所有緩衝)、beforeInvocation(是否在方法調用前就清除,預設為false,因此當方法拋出異常則緩衝不會被清掉) |
@CacheEvict(allEntries=true) |
@CacheRemoveAll |
清除所有緩衝 |
@CacheConfig |
@CacheDefaults |
在類層級上提供一些公用配置,比如value值,每個方法都一樣,就只需要在class上配置一次就好了,這個屬性很有用,可惜spring3.1不支援。 |
簡單解釋一下,上面的表示官方 文檔中的內容。左邊是spring3.1關於Cache操作的註解;中間是JSR-107規範的註解,spring在4.1版本實現;右邊是解釋。我就懶得翻譯了。
各個註解的作用與配置方法也有作者寫的不錯,我就直接拿來用了:
@Cacheable、@CachePut、@CacheEvict 注釋介紹
通過上面的例子,我們可以看到 spring cache 主要使用兩個注釋標籤,即 @Cacheable、@CachePut 和 @CacheEvict,我們總結一下其作用和配置方法。
表 1. @Cacheable 作用和配置方法
| @Cacheable 的作用 |
主要針對方法配置,能夠根據方法的請求參數對其結果進行緩衝 |
| @Cacheable 主要的參數 |
| value |
緩衝的名稱,在 spring 設定檔中定義,必須指定至少一個 |
例如: @Cacheable(value=”mycache”) 或者 @Cacheable(value={”cache1”,”cache2”} |
| key |
緩衝的 key,可以為空白,如果指定要按照 SpEL 運算式編寫,如果不指定,則預設按照方法的所有參數進行組合 |
例如: @Cacheable(value=”testcache”,key=”#userName”) |
| condition |
緩衝的條件,可以為空白,使用 SpEL 編寫,返回 true 或者 false,只有為 true 才進行緩衝 |
例如: @Cacheable(value=”testcache”,condition=”#userName.length()>2”) |
表 2. @CachePut 作用和配置方法
| @CachePut 的作用 |
主要針對方法配置,能夠根據方法的請求參數對其結果進行緩衝,和 @Cacheable 不同的是,它每次都會觸發真實方法的調用 |
| @CachePut 主要的參數 |
| value |
緩衝的名稱,在 spring 設定檔中定義,必須指定至少一個 |
例如: @Cacheable(value=”mycache”) 或者 @Cacheable(value={”cache1”,”cache2”} |
| key |
緩衝的 key,可以為空白,如果指定要按照 SpEL 運算式編寫,如果不指定,則預設按照方法的所有參數進行組合 |
例如: @Cacheable(value=”testcache”,key=”#userName”) |
| condition |
緩衝的條件,可以為空白,使用 SpEL 編寫,返回 true 或者 false,只有為 true 才進行緩衝 |
例如: @Cacheable(value=”testcache”,condition=”#userName.length()>2”) |
表 3. @CacheEvict 作用和配置方法
| @CachEvict 的作用 |
主要針對方法配置,能夠根據一定的條件對緩衝進行清空 |
| @CacheEvict 主要的參數 |
| value |
緩衝的名稱,在 spring 設定檔中定義,必須指定至少一個 |
例如: @CachEvict(value=”mycache”) 或者 @CachEvict(value={”cache1”,”cache2”} |
| key |
緩衝的 key,可以為空白,如果指定要按照 SpEL 運算式編寫,如果不指定,則預設按照方法的所有參數進行組合 |
例如: @CachEvict(value=”testcache”,key=”#userName”) |
| condition |
緩衝的條件,可以為空白,使用 SpEL 編寫,返回 true 或者 false,只有為 true 才清空緩衝 |
例如: @CachEvict(value=”testcache”, condition=”#userName.length()>2”) |
| allEntries |
是否清空所有緩衝內容,預設為 false,如果指定為 true,則方法調用後將立即清空所有緩衝 |
例如: @CachEvict(value=”testcache”,allEntries=true) |
| beforeInvocation |
是否在方法執行前就清空,預設為 false,如果指定為 true,則在方法還沒有執行的時候就清空緩衝,預設情況下,如果方法執行拋出異常,則不會清空緩衝 |
例如:
@CachEvict(value=”testcache”,beforeInvocation=true) |
關於上面的註解作用與含義,沒有多少再補充解釋的了,原作者的 文章介紹的也很詳細。這裡補充一下spring-cache的cache配置。cache元素配置除過cache-manager屬性外,還有許多屬性,簡單羅列如下:
XML屬性 |
註解屬性 |
預設值 |
含義 |
cache-manager |
無 |
cacheManager |
預設的cacheManager名稱。一個預設的CacheResolver在cacheManager的後台被初始化,要想更精細的管理緩衝可以考慮設定cache-resolver屬性 |
cache-resolver |
無 |
SimpleCacheResolver |
CacheResolver的bean名稱,這個非必需屬性,僅僅作為cache-manager屬性的替代 |
|