SpringCache扩展:注解失效时间+主动刷新缓存
Spring Cache 两个需求
缓存失效时间⽀持在⽅法的注解上指定
Spring Cache默认是不⽀持在@Cacheable上添加过期时间的,可以在配置缓存容器时统⼀指定:
@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")
上⾯两种各有利弊,并不是说哪⼀种⼀定要⽐另外⼀种强,根据⾃⼰项⽬的实际情况选择。
在缓存即将过期时主动刷新缓存
⼀般缓存失效后,会有⼀些请求会打到后端的数据库上,这段时间的访问性能肯定是⽐有缓存的情况要差很多。所以期望在缓存即将过期的某⼀时间点后台主动去更新缓存以确保前端请求的缓存命中率,⽰意图如下:
Srping 4.3提供了⼀个sync参数。是当缓存失效后,为了避免多个请求打到数据库,系统做了⼀个并发控制优化,同时只有⼀个线程会去数据库取数据其它线程会被阻塞。
背景
我以Spring Cache +Redis为前提来实现上⾯两个需求,其它类型的缓存原理应该是相同的。
本⽂内容未在⽣产环境验证过,也许有不妥的地⽅,请多多指出。
扩展RedisCacheManager
CustomizedRedisCacheManager
继承⾃RedisCacheManager,定义两个辅助性的属性:
/**
* 缓存参数的分隔符
* 数组元素0=缓存的名称
* 数组元素1=缓存过期时间TTL
* 数组元素2=缓存在多少秒开始主动失效来强制刷新
*/
private String separator = "#";
/**
* 缓存主动在失效前强制刷新缓存的时间
* 单位:秒
*/
private long preloadSecondTime=0;
注解配置失效时间简单的⽅法就是在容器名称上动动⼿脚,通过解析特定格式的名称来变向实现失效时间的获取。⽐如第⼀个#后⾯的5可以定义为失效时间,第⼆个#后⾯的2是刷新缓存的时间,只需要重写getCache:
解析配置的value值,分别计算出真正的缓存名称,失效时间以及缓存刷新的时间
调⽤构造函数返回缓存对象
@Override
public Cache getCache(String name) {
String[] cacheParams=name.Separator());
String cacheName = cacheParams[0];
if(StringUtils.isBlank(cacheName)){
return null;
}
Long expirationSecondTime = thisputeExpiration(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 = Cache(cacheName);
if(null==cache){
return cache;
}
logger.info("expirationSecondTime:"+expirationSecondTime);
CustomizedRedisCache redisCache= new CustomizedRedisCache(
cacheName,
(this.isUsePrefix() ? CachePrefix().prefix(cacheName) : null),
cacheable
expirationSecondTime,
preloadSecondTime);
return redisCache;
}
CustomizedRedisCache
主要是实现缓存即将过期时能够主动触发缓存更新,核⼼是下⾯这个get⽅法。在获取到缓存后再次取缓存剩余的时间,如果时间⼩余我们配置的刷新时间就⼿动刷新缓存。为了不影响get的性能,启⽤后台线程去完成缓存的刷新。
public ValueWrapper get(Object key) {
ValueWrapper valueWrapper= (key);
if(null!=valueWrapper){
Long ttl= Expire(key);
if(null!=ttl&& ttl<=this.preloadSecondTime){
logger.info("key:{} ttl:{} preloadSecondTime:{}",key,ttl,preloadSecondTime);
ThreadTaskHelper.run(new Runnable() {
@Override
public void run() {
//重新加载数据
logger.info("refresh key:{}",key);
CacheSupport().refreshCacheByKey(Name(),String());
}
});
}
}
return valueWrapper;
}
ThreadTaskHelper是个帮助类,但需要考虑重复请求问题,及相同的数据在并发过程中只允许刷新⼀次,这块还没有完善就不贴代码了。
拦截@Cacheable,并记录执⾏⽅法信息
上⾯提到的缓存获取时,会根据配置的刷新时间来判断是否需要刷新数据,当符合条件时会触发数据刷新。但它需要知道执⾏什么⽅法以及更新哪些数据,所以就有了下⾯这些类。
CacheSupport
刷新缓存接⼝,可刷新整个容器的缓存也可以只刷新指定键的缓存。
public interface CacheSupport {
/**
* 刷新容器中所有值
* @param cacheName
*/
void refreshCache(String cacheName);
/**
* 按容器以及指定键更新缓存
* @param cacheName
* @param cacheKey
*/
void refreshCacheByKey(String cacheName,String cacheKey);
}
InvocationRegistry
执⾏⽅法注册接⼝,能够在适当的地⽅主动调⽤⽅法执⾏来完成缓存的更新。
public interface InvocationRegistry {
void registerInvocation(Object invokedBean, Method invokedMethod, Object[] invocationArguments, Set<String> cacheNames);
}
CachedInvocation
执⾏⽅法信息类,这个⽐较简单,就是满⾜⽅法执⾏的所有信息即可。
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 = pyOf(arguments, arguments.length);
}
}
}
CacheSupportImpl
这个类主要实现上⾯定义的缓存刷新接⼝以及执⾏⽅法注册接⼝
刷新缓存
获取cacheManager⽤来操作缓存:
@Autowired
private CacheManager cacheManager;
实现缓存刷新接⼝⽅法:
@Override
public void refreshCache(String cacheName) {
}
@Override
public void refreshCacheByKey(String cacheName, String cacheKey) {
if ((cacheName) != null) {
for (final CachedInvocation invocation : (cacheName)) {
if(!StringUtils.isBlank(cacheKey)&&Key().toString().equals(cacheKey)) {
refreshCache(invocation, cacheName);
}
}
}
}
反射来调⽤⽅法:
private Object invoke(CachedInvocation invocation)
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
final MethodInvoker invoker = new MethodInvoker();
invoker.TargetBean());
invoker.Arguments());
invoker.TargetMethod().getName());
invoker.prepare();
return invoker.invoke();
}
缓存刷新最后实际执⾏是这个⽅法,通过invoke函数获取到最新的数据,然后通过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 ((cacheName) != null) {
}
}
}
执⾏⽅法信息注册

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。