Spring cache是對緩存使用的抽象,通過它我們可以在不侵入業務代碼的基礎上讓現有代碼即刻支持緩存。但是使用中,我發現Spring cache不支持對每個緩存key設置失效時間(只支持設置一個全局的失效時間),所以我產生了重復造輪子的沖動,于是就有了這篇文章
Spring cache 簡介
Spring cache主要提供了以下幾個Annotation:
注解 | 適用的方法類型 | 作用 |
---|---|---|
@Cacheable | 讀數據 | 方法被調用時,先從緩存中讀取數據,如果緩存沒有找到數據,再調用方法獲取數據,然后把數據添加到緩存中 |
@CachePut | 寫數據:如新增/修改數據 | 調用方法時會自動把相應的數據放入緩存 |
@CacheEvict | 刪除數據 | 調用方法時會從緩存中移除相應的數據 |
網上介紹Spring cache的文章很多,所以本文不想在這里進行太多的介紹。有興趣的童鞋可以參閱一下以下幾篇博文:注釋驅動的 Spring cache 緩存介紹 ,Spring Cache抽象詳解 ,Spring cache官方文檔
Spring cache官方文檔里有專門解釋它為什么沒有提供設置失效時間的功能:
--- 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 cache只是對緩存的抽象,并不是緩存的一個具體實現。不同的具體緩存實現方案可能會有各自不同的特性。Spring cache作為緩存的抽象,它只能抽象出共同的屬性/功能,對于無法統一的那部分屬性/功能它就無能為力了,這部分差異化的屬性/功能應該由具體的緩存實現者去配置。如果Spring cache提供一些差異化的屬性,那么有一些緩存提供者不支持這個屬性怎么辦? 所以Spring cache就干脆不提供這些配置了。
這就解釋了Spring cache不提供緩存失效時間相關配置的原因:因為并不是所有的緩存實現方案都支持設置緩存的失效時間。
為了既能使用Spring cache提供的便利,又能簡單的設置緩存失效時間,網上我也搜到了一些別人的解決方案:spring-data-redis 擴展實現時效設置,擴展基于注解的spring緩存,使緩存有效期的設置支持方法級別-redis篇??催^別人的解決方案后我覺得都不是太完美,并且我還想進一步實現其他的一些特性,所以我決定還是自己動手了。
實現Spring Cache
首先,我們定義自己的@Cacheable注解:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {
/**
* 緩存命名空間
* 如果是本地緩存,namespace相同的緩存數據存放在同一個map中
* 如果是redis緩存,緩存的key將會加上namespace作為前綴,為:{namespace}:key
* @return
*/
String namespace();
/**
* 緩存key表達式,支持常量字符串/請求參數表達式
* @return
*/
String key();
/**
* 存儲類型:Local(本地緩存) or Redis緩存
* @return
*/
Storage storage() default Storage.redis;
/**
* 緩存存活時間,單位秒:支持數字/數字表達式或本地配置文件參數
* 例如:${orderCacheTTL:10},其中orderCacheTTL為本地配置文件中的配置參數,10為默認值
* @return
*/
String ttl() default "0";
}
public enum Storage {
local, redis
}
對比一下Spring cache中對@Cacheable的聲明,可以看出二者除了注解名字相同,其它的基本都不一樣了。下面對@Cacheable先做一下詳細說明:
- @Cacheable的作用跟Spring中的一樣,都是作用于“讀方法”之上,能夠透明地給該方法加上緩存功能
- storage屬性,支持兩種值:local(本地緩存),redis(redis緩存),這樣給本地緩存,redis緩存對外提供了一致的使用方式,能夠很方便靈活地在二者中切換
- namespace屬性:給緩存增加了命名空間的概念,減少緩存key沖突的問題
- key屬性:緩存的key,支持常量字符串及簡單的表達式。例如:我們如果需要緩存的數據是不隨請求參數改變而變化的,緩存的key可以是一個常量字符串。如下面的例子:
//將會被緩存在redis中,緩存失效時間是24小時,在redis中的key為:common:books,值為json字符串
@Cacheable(namespace="common", key="books", ttl="24*60*60", storage=Storage.redis)
public List<Book> findBooks(){
//(代碼略)
}
- key也支持簡單的表達式(目前我實現的版本支持的表達式形式沒有Spring cache那么強大,因為我覺得夠用了):使用$0,$1,$2......分別表示方法的第一個參數值,第二個參數值,第三個參數值......。比如:$0.name,表示第一個請求參數的name屬性的值作為緩存的key;$1.generateKey(),表示執行第二個請求參數的generateKey()方法的返回值作為緩存的key。如下面的例子:
//將會被緩存在redis中,緩存失效時間是600s,請求參數為user.id,比如:請求參數user的id=1 則在redis中的key為:accounts:1,值為json字符串
@Cacheable(namespace="accounts", key="$0.id", ttl="600", storage=Storage.redis)
public Account getAccount(User user){
//(代碼略)
}
- ttl屬性:緩存的失效時間。既支持簡單的數字,也支持數字表達式,還支持配置文件參數(畢竟是自己實現的,想支持什么都可以實現)
好了,@Cacheable講解完了,下面來看一下是怎么實現聲明式緩存的吧,先貼代碼:
@Component
public class CacheableMethodHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
public Object handle(MethodInvocation invocation) throws Throwable {
Object result = null;
Cacheable cacheable = null;
String key = null;
Cache cache = null;
try {
Method method = invocation.getMethod();
cacheable = method.getAnnotation(Cacheable.class);
key = ExpressParserSupport.parseKey(cacheable.key(), invocation.getArguments());
cache = CacheFactory.getCache(cacheable.storage(), cacheable.namespace());
CachedValue cachedValue = cache.get(key);
if (cachedValue == null || cachedValue.getValue() == null) {
return this.handleCacheMissing(cacheable, cache, key, invocation);
}
if (this.isCacheExpired(cachedValue)) {
return this.handleCacheExpired(cacheable, cache, key, cachedValue, invocation);
}
return this.handleCacheHit(cachedValue, method.getGenericReturnType());
} catch (MethodExecuteException e1) {
logger.error("Exception occurred when execute proxied method", e1);
throw e1.getCause();
} catch (Exception e2) {
logger.error("Exception occurred when handle cache", e2);
result = invocation.proceed();
this.putIntoCache(cacheable, cache, key, result);
}
return result;
}
private Object handleCacheHit(CachedValue cachedValue, Type returnType) {
Object value = cachedValue.getValue();
return ValueDeserializeSupport.deserialize(value, returnType);
}
private Object handleCacheExpired(Cacheable cacheable, Cache cache, String key, CachedValue cachedValue,
MethodInvocation invocation) {
return this.handleCacheMissing(cacheable, cache, key, invocation);
}
private Object handleCacheMissing(Cacheable cacheable, Cache cache, String key, MethodInvocation invocation) {
Object result = this.executeProxiedTargetMethod(invocation);
if (result != null) {
this.putIntoCache(cacheable, cache, key, result);
}
return result;
}
private void putIntoCache(Cacheable cacheable, Cache cache, String key, Object value) {
try {
CachedValue cachedValue = this.wrapValue(value, ExpressParserSupport.parseTtl(cacheable.ttl()));
cache.put(key, cachedValue);
} catch (Exception e) {
logger.error("Put into cache error", e);
}
}
private Object executeProxiedTargetMethod(MethodInvocation invocation) {
try {
return invocation.proceed();
} catch (Throwable throwable) {
throw new MethodExecuteException(throwable);
}
}
private boolean isCacheExpired(CachedValue cachedValue) {
return (cachedValue.getExpiredAt() > 0 && System.currentTimeMillis() > cachedValue.getExpiredAt());
}
private CachedValue wrapValue(Object value, int ttl) {
CachedValue cachedValue = new CachedValue();
cachedValue.setValue(value);
cachedValue.setTtl(ttl);
return cachedValue;
}
}
@Component
public class CachePointcutAdvisor extends AbstractPointcutAdvisor {
private final StaticMethodMatcherPointcut pointcut = new StaticMethodMatcherPointcut() {
@Override
public boolean matches(Method method, Class<?> targetClass) {
return method.isAnnotationPresent(Cacheable.class) || method.isAnnotationPresent(CachePut.class)
|| method.isAnnotationPresent(CacheEvict.class);
}
};
@Autowired
private CacheMethodInterceptor cacheMethodInterceptor;
@Override
public Pointcut getPointcut() {
return pointcut;
}
@Override
public Advice getAdvice() {
return cacheMethodInterceptor;
}
}
可以看到,這里是利用了Spring提供的AOP來實現的。通過AOP來達到對聲明了@Cacheable方法的攔截及代理,代碼邏輯也比較簡單:先根據namespace及key查看是否存在緩存,如果存在且緩存尚未失效則直接返回緩存值;否則直接執行被代理類對象的方法并緩存執行結果然后再返回執行結果。
在這里面,其實也有很多可以優化或擴展的點,比如我們可以針對緩存失效的情況進行進一步擴展:如果發現緩存已失效,可以先返回已“過期”的數據,然后通過后臺線程異步刷新緩存
下面貼一下方法攔截器里面使用到的其它關鍵類代碼:
//Cache接口聲明
public interface Cache {
String getName();
CachedValue get(String key);
void put(String key, CachedValue value);
void clear();
void delete(String key);
}
//根據命名空間及緩存類型查找或新生成對應的Cache
public class CacheFactory {
private static LocalCacheManager localCacheManager = new LocalCacheManager();
private static RedisCacheManager redisCacheManager = new RedisCacheManager();
private CacheFactory() {}
public static Cache getCache(Storage storage, String namespace) {
if (storage == Storage.local) {
return localCacheManager.getCache(namespace);
} else if (storage == Storage.redis) {
return redisCacheManager.getCache(namespace);
} else {
throw new IllegalArgumentException("Unknown storage type:" + storage);
}
}
}
//Redis Cache的實現
public class RedisCache implements Cache {
private String namespace;
private RedisCacheProvider cacheProvider;
public RedisCache(String namespace) {
this.namespace = namespace;
this.cacheProvider = new RedisCacheProvider();
}
@Override
public String getName() {
return "Redis:" + this.namespace;
}
@Override
public CachedValue get(String key) {
String value = this.cacheProvider.get(this.buildCacheKey(key));
CachedValue cachedValue = null;
if (value != null) {
cachedValue = new CachedValue();
cachedValue.setValue(value);
}
return cachedValue;
}
@Override
public void put(String key, CachedValue cachedValue) {
Object value = cachedValue.getValue();
if (value != null) {
String json;
if (value instanceof String) {
json = (String) value;
} else {
json = JsonUtil.toJson(value);
}
String cacheKey = this.buildCacheKey(key);
if (cachedValue.getTtl() > 0) {
this.cacheProvider.setex(cacheKey, cachedValue.getTtl(), json);
} else {
this.cacheProvider.set(cacheKey, json);
}
}
}
@Override
public void clear() {
//do nothing
}
@Override
public void delete(String key) {
this.cacheProvider.del(this.buildCacheKey(key));
}
private String buildCacheKey(String key) {
return this.namespace+":"+ key;
}
}
其它的關于key表達式/ttl表達式的解析(關于表達式的解析大家可以參考Spring Expression Language官方文檔),緩存的值的序列化反序列化等代碼因為篇幅關系就不貼出來了。有疑問的童鞋可以回帖問我。
當然,這里實現的Cache的功能跟Spring cache還是欠缺很多,但是我覺得已經能覆蓋大部分應用場景了。希望通過本文的拋磚,能夠給大家一點啟發。