Spring Cache扩展功能实现过程解析
SpringCache默认是不支持在@Cacheable上添加过期时间的,可以在配置缓存容器时统一指定:
@Bean publicCacheManagercacheManager( @SuppressWarnings("rawtypes")RedisTemplateredisTemplate){ CustomizedRedisCacheManagercacheManager=newCustomizedRedisCacheManager(redisTemplate); cacheManager.setDefaultExpiration(60); MapexpiresMap=newHashMap<>(); expiresMap.put("Product",5L); cacheManager.setExpires(expiresMap); returncacheManager; }
想这样配置过期时间,焦点在value的格式上Product#5#2,详情下面会详细说明。
@Cacheable(value={"Product#5#2"},key="#id")
上面两种各有利弊,并不是说哪一种一定要比另外一种强,根据自己项目的实际情况选择。
在缓存即将过期时主动刷新缓存
一般缓存失效后,会有一些请求会打到后端的数据库上,这段时间的访问性能肯定是比有缓存的情况要差很多。所以期望在缓存即将过期的某一时间点后台主动去更新缓存以确保前端请求的缓存命中率,示意图如下:
Srping4.3提供了一个sync参数。是当缓存失效后,为了避免多个请求打到数据库,系统做了一个并发控制优化,同时只有一个线程会去数据库取数据其它线程会被阻塞。
背景
我以SpringCache+Redis为前提来实现上面两个需求,其它类型的缓存原理应该是相同的。
本文内容未在生产环境验证过,也许有不妥的地方,请多多指出。
扩展RedisCacheManagerCustomizedRedisCacheManager
继承自RedisCacheManager,定义两个辅助性的属性:
/** *缓存参数的分隔符 *数组元素0=缓存的名称 *数组元素1=缓存过期时间TTL *数组元素2=缓存在多少秒开始主动失效来强制刷新 */ privateStringseparator="#"; /** *缓存主动在失效前强制刷新缓存的时间 *单位:秒 */ privatelongpreloadSecondTime=0;
注解配置失效时间简单的方法就是在容器名称上动动手脚,通过解析特定格式的名称来变向实现失效时间的获取。比如第一个#后面的5可以定义为失效时间,第二个#后面的2是刷新缓存的时间,只需要重写getCache:
- 解析配置的value值,分别计算出真正的缓存名称,失效时间以及缓存刷新的时间
- 调用构造函数返回缓存对象
@Override publicCachegetCache(Stringname){ String[]cacheParams=name.split(this.getSeparator()); StringcacheName=cacheParams[0]; if(StringUtils.isBlank(cacheName)){ returnnull; } LongexpirationSecondTime=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])); } Cachecache=super.getCache(cacheName); if(null==cache){ returncache; } logger.info("expirationSecondTime:"+expirationSecondTime); CustomizedRedisCacheredisCache=newCustomizedRedisCache( cacheName, (this.isUsePrefix()?this.getCachePrefix().prefix(cacheName):null), this.getRedisOperations(), expirationSecondTime, preloadSecondTime); returnredisCache; }
CustomizedRedisCache
主要是实现缓存即将过期时能够主动触发缓存更新,核心是下面这个get方法。在获取到缓存后再次取缓存剩余的时间,如果时间小余我们配置的刷新时间就手动刷新缓存。为了不影响get的性能,启用后台线程去完成缓存的刷新。
publicValueWrapperget(Objectkey){ ValueWrappervalueWrapper=super.get(key); if(null!=valueWrapper){ Longttl=this.redisOperations.getExpire(key); if(null!=ttl&&ttl<=this.preloadSecondTime){ logger.info("key:{}ttl:{}preloadSecondTime:{}",key,ttl,preloadSecondTime); ThreadTaskHelper.run(newRunnable(){ @Override publicvoidrun(){ //重新加载数据 logger.info("refreshkey:{}",key); CustomizedRedisCache.this.getCacheSupport().refreshCacheByKey(CustomizedRedisCache.super.getName(),key.toString()); } }); } } returnvalueWrapper; }
ThreadTaskHelper是个帮助类,但需要考虑重复请求问题,及相同的数据在并发过程中只允许刷新一次,这块还没有完善就不贴代码了。
拦截@Cacheable,并记录执行方法信息
上面提到的缓存获取时,会根据配置的刷新时间来判断是否需要刷新数据,当符合条件时会触发数据刷新。但它需要知道执行什么方法以及更新哪些数据,所以就有了下面这些类。
CacheSupport
刷新缓存接口,可刷新整个容器的缓存也可以只刷新指定键的缓存。
publicinterfaceCacheSupport{ /** *刷新容器中所有值 *@paramcacheName */ voidrefreshCache(StringcacheName); /** *按容器以及指定键更新缓存 *@paramcacheName *@paramcacheKey */ voidrefreshCacheByKey(StringcacheName,StringcacheKey); }
InvocationRegistry
执行方法注册接口,能够在适当的地方主动调用方法执行来完成缓存的更新。
publicinterfaceInvocationRegistry{ voidregisterInvocation(ObjectinvokedBean,MethodinvokedMethod,Object[]invocationArguments,SetcacheNames); }
CachedInvocation
执行方法信息类,这个比较简单,就是满足方法执行的所有信息即可。
publicfinalclassCachedInvocation{ privateObjectkey; privatefinalObjecttargetBean; privatefinalMethodtargetMethod; privateObject[]arguments; publicCachedInvocation(Objectkey,ObjecttargetBean,MethodtargetMethod,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
这个类主要实现上面定义的缓存刷新接口以及执行方法注册接口
刷新缓存
获取cacheManager用来操作缓存:
@Autowired privateCacheManagercacheManager;
实现缓存刷新接口方法:
@Override publicvoidrefreshCache(StringcacheName){ this.refreshCacheByKey(cacheName,null); } @Override publicvoidrefreshCacheByKey(StringcacheName,StringcacheKey){ if(cacheToInvocationsMap.get(cacheName)!=null){ for(finalCachedInvocationinvocation:cacheToInvocationsMap.get(cacheName)){ if(!StringUtils.isBlank(cacheKey)&&invocation.getKey().toString().equals(cacheKey)){ refreshCache(invocation,cacheName); } } } }
反射来调用方法:
privateObjectinvoke(CachedInvocationinvocation) throwsClassNotFoundException,NoSuchMethodException,InvocationTargetException,IllegalAccessException{ finalMethodInvokerinvoker=newMethodInvoker(); invoker.setTargetObject(invocation.getTargetBean()); invoker.setArguments(invocation.getArguments()); invoker.setTargetMethod(invocation.getTargetMethod().getName()); invoker.prepare(); returninvoker.invoke(); }
缓存刷新最后实际执行是这个方法,通过invoke函数获取到最新的数据,然后通过cacheManager来完成缓存的更新操作。
privatevoidrefreshCache(CachedInvocationinvocation,StringcacheName){ booleaninvocationSuccess; Objectcomputed=null; try{ computed=invoke(invocation); invocationSuccess=true; }catch(Exceptionex){ invocationSuccess=false; } if(invocationSuccess){ if(cacheToInvocationsMap.get(cacheName)!=null){ cacheManager.getCache(cacheName).put(invocation.getKey(),computed); } } }
执行方法信息注册
定义一个Map用来存储执行方法的信息:
privateMap
实现执行方法信息接口,构造执行方法对象然后存储到Map中。
@Override publicvoidregisterInvocation(ObjecttargetBean,MethodtargetMethod,Object[]arguments,SetannotatedCacheNames){ StringBuildersb=newStringBuilder(); for(Objectobj:arguments){ sb.append(obj.toString()); } Objectkey=sb.toString(); finalCachedInvocationinvocation=newCachedInvocation(key,targetBean,targetMethod,arguments); for(finalStringcacheName:annotatedCacheNames){ String[]cacheParams=cacheName.split("#"); StringrealCacheName=cacheParams[0]; if(!cacheToInvocationsMap.containsKey(realCacheName)){ this.initialize(); } cacheToInvocationsMap.get(realCacheName).add(invocation); } }
CachingAnnotationsAspect
拦截@Cacheable方法信息并完成注册,将使用了缓存的方法的执行信息存储到Map中,key是缓存容器的名称,value是不同参数的方法执行实例,核心方法就是registerInvocation。
@Around("pointcut()") publicObjectregisterInvocation(ProceedingJoinPointjoinPoint)throwsThrowable{ Methodmethod=this.getSpecificmethod(joinPoint); Listannotations=this.getMethodAnnotations(method,Cacheable.class); Set cacheSet=newHashSet (); for(Cacheablecacheables:annotations){ cacheSet.addAll(Arrays.asList(cacheables.value())); } cacheRefreshSupport.registerInvocation(joinPoint.getTarget(),method,joinPoint.getArgs(),cacheSet); returnjoinPoint.proceed(); }
客户端调用
指定5秒后过期,并且在缓存存活3秒后如果请求命中,会在后台启动线程重新从数据库中获取数据来完成缓存的更新。理论上前端不会存在缓存不命中的情况,当然如果正好最后两秒没有请求那也会出现缓存失效的情况。
@Cacheable(value={"Product#5#2"},key="#id") publicProductgetById(Longid){ //... }
代码
可以从项目中下载。
引用
刷新缓存的思路取自于这个开源项目。https://github.com/yantrashala/spring-cache-self-refresh
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。