Spring Cache擴展功能實現(xiàn)過程解析
兩個需求緩存失效時間支持在方法的注解上指定
Spring Cache默認是不支持在@Cacheable上添加過期時間的,可以在配置緩存容器時統(tǒng)一指定:
@Bean
public CacheManager cacheManager(
@SuppressWarnings("rawtypes") RedisTemplate redisTemplate) {
CustomizedRedisCacheManager cacheManager= new CustomizedRedisCacheManager(redisTemplate);
cacheManager.setDefaultExpiration(60);
Map<String,Long> expiresMap=new HashMap<>();
expiresMap.put("Product",5L);
cacheManager.setExpires(expiresMap);
return cacheManager;
}
想這樣配置過期時間,焦點在value的格式上Product#5#2,詳情下面會詳細說明。
@Cacheable(value = {"Product#5#2"},key ="#id")
上面兩種各有利弊,并不是說哪一種一定要比另外一種強,根據(jù)自己項目的實際情況選擇。
在緩存即將過期時主動刷新緩存
一般緩存失效后,會有一些請求會打到后端的數(shù)據(jù)庫上,這段時間的訪問性能肯定是比有緩存的情況要差很多。所以期望在緩存即將過期的某一時間點后臺主動去更新緩存以確保前端請求的緩存命中率,示意圖如下:

Srping 4.3提供了一個sync參數(shù)。是當(dāng)緩存失效后,為了避免多個請求打到數(shù)據(jù)庫,系統(tǒng)做了一個并發(fā)控制優(yōu)化,同時只有一個線程會去數(shù)據(jù)庫取數(shù)據(jù)其它線程會被阻塞。
背景
我以Spring Cache +Redis為前提來實現(xiàn)上面兩個需求,其它類型的緩存原理應(yīng)該是相同的。
本文內(nèi)容未在生產(chǎn)環(huán)境驗證過,也許有不妥的地方,請多多指出。
擴展RedisCacheManagerCustomizedRedisCacheManager
繼承自RedisCacheManager,定義兩個輔助性的屬性:
/** * 緩存參數(shù)的分隔符 * 數(shù)組元素0=緩存的名稱 * 數(shù)組元素1=緩存過期時間TTL * 數(shù)組元素2=緩存在多少秒開始主動失效來強制刷新 */ private String separator = "#"; /** * 緩存主動在失效前強制刷新緩存的時間 * 單位:秒 */ private long preloadSecondTime=0;
注解配置失效時間簡單的方法就是在容器名稱上動動手腳,通過解析特定格式的名稱來變向?qū)崿F(xiàn)失效時間的獲取。比如第一個#后面的5可以定義為失效時間,第二個#后面的2是刷新緩存的時間,只需要重寫getCache:
- 解析配置的value值,分別計算出真正的緩存名稱,失效時間以及緩存刷新的時間
- 調(diào)用構(gòu)造函數(shù)返回緩存對象
@Override
public Cache getCache(String name) {
String[] cacheParams=name.split(this.getSeparator());
String cacheName = cacheParams[0];
if(StringUtils.isBlank(cacheName)){
return null;
}
Long expirationSecondTime = this.computeExpiration(cacheName);
if(cacheParams.length>1) {
expirationSecondTime=Long.parseLong(cacheParams[1]);
this.setDefaultExpiration(expirationSecondTime);
}
if(cacheParams.length>2) {
this.setPreloadSecondTime(Long.parseLong(cacheParams[2]));
}
Cache cache = super.getCache(cacheName);
if(null==cache){
return cache;
}
logger.info("expirationSecondTime:"+expirationSecondTime);
CustomizedRedisCache redisCache= new CustomizedRedisCache(
cacheName,
(this.isUsePrefix() ? this.getCachePrefix().prefix(cacheName) : null),
this.getRedisOperations(),
expirationSecondTime,
preloadSecondTime);
return redisCache;
}
CustomizedRedisCache
主要是實現(xiàn)緩存即將過期時能夠主動觸發(fā)緩存更新,核心是下面這個get方法。在獲取到緩存后再次取緩存剩余的時間,如果時間小余我們配置的刷新時間就手動刷新緩存。為了不影響get的性能,啟用后臺線程去完成緩存的刷新。
public ValueWrapper get(Object key) {
ValueWrapper valueWrapper= super.get(key);
if(null!=valueWrapper){
Long ttl= this.redisOperations.getExpire(key);
if(null!=ttl&& ttl<=this.preloadSecondTime){
logger.info("key:{} ttl:{} preloadSecondTime:{}",key,ttl,preloadSecondTime);
ThreadTaskHelper.run(new Runnable() {
@Override
public void run() {
//重新加載數(shù)據(jù)
logger.info("refresh key:{}",key);
CustomizedRedisCache.this.getCacheSupport().refreshCacheByKey(CustomizedRedisCache.super.getName(),key.toString());
}
});
}
}
return valueWrapper;
}
ThreadTaskHelper是個幫助類,但需要考慮重復(fù)請求問題,及相同的數(shù)據(jù)在并發(fā)過程中只允許刷新一次,這塊還沒有完善就不貼代碼了。
攔截@Cacheable,并記錄執(zhí)行方法信息
上面提到的緩存獲取時,會根據(jù)配置的刷新時間來判斷是否需要刷新數(shù)據(jù),當(dāng)符合條件時會觸發(fā)數(shù)據(jù)刷新。但它需要知道執(zhí)行什么方法以及更新哪些數(shù)據(jù),所以就有了下面這些類。
CacheSupport
刷新緩存接口,可刷新整個容器的緩存也可以只刷新指定鍵的緩存。
public interface CacheSupport {
/**
* 刷新容器中所有值
* @param cacheName
*/
void refreshCache(String cacheName);
/**
* 按容器以及指定鍵更新緩存
* @param cacheName
* @param cacheKey
*/
void refreshCacheByKey(String cacheName,String cacheKey);
}
InvocationRegistry
執(zhí)行方法注冊接口,能夠在適當(dāng)?shù)牡胤街鲃诱{(diào)用方法執(zhí)行來完成緩存的更新。
public interface InvocationRegistry {
void registerInvocation(Object invokedBean, Method invokedMethod, Object[] invocationArguments, Set<String> cacheNames);
}
CachedInvocation
執(zhí)行方法信息類,這個比較簡單,就是滿足方法執(zhí)行的所有信息即可。
public final class CachedInvocation {
private Object key;
private final Object targetBean;
private final Method targetMethod;
private Object[] arguments;
public CachedInvocation(Object key, Object targetBean, Method targetMethod, Object[] arguments) {
this.key = key;
this.targetBean = targetBean;
this.targetMethod = targetMethod;
if (arguments != null && arguments.length != 0) {
this.arguments = Arrays.copyOf(arguments, arguments.length);
}
}
}
CacheSupportImpl
這個類主要實現(xiàn)上面定義的緩存刷新接口以及執(zhí)行方法注冊接口
刷新緩存
獲取cacheManager用來操作緩存:
@Autowired private CacheManager cacheManager;
實現(xiàn)緩存刷新接口方法:
@Override
public void refreshCache(String cacheName) {
this.refreshCacheByKey(cacheName,null);
}
@Override
public void refreshCacheByKey(String cacheName, String cacheKey) {
if (cacheToInvocationsMap.get(cacheName) != null) {
for (final CachedInvocation invocation : cacheToInvocationsMap.get(cacheName)) {
if(!StringUtils.isBlank(cacheKey)&&invocation.getKey().toString().equals(cacheKey)) {
refreshCache(invocation, cacheName);
}
}
}
}
反射來調(diào)用方法:
private Object invoke(CachedInvocation invocation)
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
final MethodInvoker invoker = new MethodInvoker();
invoker.setTargetObject(invocation.getTargetBean());
invoker.setArguments(invocation.getArguments());
invoker.setTargetMethod(invocation.getTargetMethod().getName());
invoker.prepare();
return invoker.invoke();
}
緩存刷新最后實際執(zhí)行是這個方法,通過invoke函數(shù)獲取到最新的數(shù)據(jù),然后通過cacheManager來完成緩存的更新操作。
private void refreshCache(CachedInvocation invocation, String cacheName) {
boolean invocationSuccess;
Object computed = null;
try {
computed = invoke(invocation);
invocationSuccess = true;
} catch (Exception ex) {
invocationSuccess = false;
}
if (invocationSuccess) {
if (cacheToInvocationsMap.get(cacheName) != null) {
cacheManager.getCache(cacheName).put(invocation.getKey(), computed);
}
}
}
執(zhí)行方法信息注冊
定義一個Map用來存儲執(zhí)行方法的信息:
private Map<String, Set<CachedInvocation>> cacheToInvocationsMap;
實現(xiàn)執(zhí)行方法信息接口,構(gòu)造執(zhí)行方法對象然后存儲到Map中。
@Override
public void registerInvocation(Object targetBean, Method targetMethod, Object[] arguments, Set<String> annotatedCacheNames) {
StringBuilder sb = new StringBuilder();
for (Object obj : arguments) {
sb.append(obj.toString());
}
Object key = sb.toString();
final CachedInvocation invocation = new CachedInvocation(key, targetBean, targetMethod, arguments);
for (final String cacheName : annotatedCacheNames) {
String[] cacheParams=cacheName.split("#");
String realCacheName = cacheParams[0];
if(!cacheToInvocationsMap.containsKey(realCacheName)) {
this.initialize();
}
cacheToInvocationsMap.get(realCacheName).add(invocation);
}
}
CachingAnnotationsAspect
攔截@Cacheable方法信息并完成注冊,將使用了緩存的方法的執(zhí)行信息存儲到Map中,key是緩存容器的名稱,value是不同參數(shù)的方法執(zhí)行實例,核心方法就是registerInvocation。
@Around("pointcut()")
public Object registerInvocation(ProceedingJoinPoint joinPoint) throws Throwable{
Method method = this.getSpecificmethod(joinPoint);
List<Cacheable> annotations=this.getMethodAnnotations(method,Cacheable.class);
Set<String> cacheSet = new HashSet<String>();
for (Cacheable cacheables : annotations) {
cacheSet.addAll(Arrays.asList(cacheables.value()));
}
cacheRefreshSupport.registerInvocation(joinPoint.getTarget(), method, joinPoint.getArgs(), cacheSet);
return joinPoint.proceed();
}
客戶端調(diào)用
指定5秒后過期,并且在緩存存活3秒后如果請求命中,會在后臺啟動線程重新從數(shù)據(jù)庫中獲取數(shù)據(jù)來完成緩存的更新。理論上前端不會存在緩存不命中的情況,當(dāng)然如果正好最后兩秒沒有請求那也會出現(xiàn)緩存失效的情況。
@Cacheable(value = {"Product#5#2"},key ="#id")
public Product getById(Long id) {
//...
}
代碼
可以從項目中下載。

引用
刷新緩存的思路取自于這個開源項目。https://github.com/yantrashala/spring-cache-self-refresh
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Java?SpringBoot整合shiro-spring-boot-starterqi項目報錯解決
這篇文章主要介紹了Java?SpringBoot整合shiro-spring-boot-starterqi項目報錯解決,文章圍繞主題展開詳細的內(nèi)容介紹,具有一定的參考一下2022-08-08
剖析Java中HashMap數(shù)據(jù)結(jié)構(gòu)的源碼及其性能優(yōu)化
這篇文章主要介紹了Java中HashMap數(shù)據(jù)結(jié)構(gòu)的源碼及其性能優(yōu)化,文中以Java 8后HashMap的性能提升來討論了HashMap的一些優(yōu)化點,需要的朋友可以參考下2016-05-05

