springboot redis-cache 自动刷新缓存
这篇文章是对上一篇 的一个补充,上文说到 spring-data-redis-cache 虽然比较强悍,但还是有些不足的,它是一个通用的解决方案,但对于企业级的项目,住住需要解决更多的问题,常见的问题有
- 缓存预热(项目启动时加载缓存)
- 缓存穿透(空值直接穿过缓存)
- 缓存雪崩(大量缓存在同一时刻过期)
- 缓存更新(查询到的数据为旧数据问题)
- 缓存降级
- redis 缓存时,redis 内存用量问题
本文解决的问题
增强 spring-data-redis-cache 的功能,增强的功能如下
- 自定义注解实现配置缓存的过期时间
- 当取缓存数据时检测是否已经达到刷新数据阀值,如已达到,则主动刷新缓存
- 当检测到存入的数据为空数据,包含集体空,map 空,空对象,空串,空数组时,设定特定的过期时间
- 可以批量设置过期时间,使用 kryo 值序列化
- 重写了 key 生成策略,使用 md5(target+method+params)
看网上大部分文章都是互相抄袭,而且都是旧版本的,有时还有错误,本文提供一个 spring-data-redis-2.0.10.release.jar 版本的解决方案。本文代码是经过测试的,但未在线上环境验证,使用时需注意可能存在 bug 。
实现思路
过期时间的配置很简单,修改 initialcacheconfiguration
就可以实现,下面说的是刷新缓存的实现
- 拦截
@cacheable
注解,如果执行的方法是需要刷新缓存的,则注册一个methodinvoker
存储到 redis ,使用和存储 key 相同的键名再拼接一个后缀 - 当取缓存的时候,如果 key 的过期时间达到了刷新阀值,则从 redis 取到当前 cachekey 的
methodinvoker
然后执行方法 - 将上一步的值存储进缓存,并重置过期时间
引言
本文使用到的 spring 的一些方法的说明
// 可以从目标对象获取到真实的 class 对象,而不是代理 class 类对象 class<?> targetclass = aopproxyutils.ultimatetargetclass(target);
object bean = applicationcontext.getbean(targetclass); // 获取到真实的对象,而不是代理对象 object target = aopproxyutils.getsingletontarget(bean );
methodinvoker 是 spring 封装的一个用于执行方法的工具,在拦截器中,我把它序列化到 redis
methodinvoker methodinvoker = new methodinvoker(); methodinvoker.settargetclass(targetclass); methodinvoker.settargetmethod(method.getname()); methodinvoker.setarguments(args);
springcacheannotationparser 是 spring 用来解析 cache 相关注解的,我拿来解析 cachenames ,我就不需要自己来解析 cachenames 了,毕竟它可以在类上配置,解析还是有点小麻烦。
springcacheannotationparser annotationparser = new springcacheannotationparser();
实现部分
自定义注解,配置过期时间和刷新阀值
@documented @retention(retentionpolicy.runtime) @target({elementtype.method,elementtype.type}) public @interface cachecustom { /** * 缓存失效时间 * 使用 iso-8601持续时间格式 * examples: * <pre> * "pt20.345s" -- parses as "20.345 seconds" * "pt15m" -- parses as "15 minutes" (where a minute is 60 seconds) * "pt10h" -- parses as "10 hours" (where an hour is 3600 seconds) * "p2d" -- parses as "2 days" (where a day is 24 hours or 86400 seconds) * "p2dt3h4m" -- parses as "2 days, 3 hours and 4 minutes" * "p-6h3m" -- parses as "-6 hours and +3 minutes" * "-p6h3m" -- parses as "-6 hours and -3 minutes" * "-p-6h+3m" -- parses as "+6 hours and -3 minutes" * </pre> * @return */ string expire() default "pt60s"; /** * 刷新时间阀值,不配置将不会进行缓存刷新 * 对于像前端的分页条件查询,建议不配置,这将在内存生成一个执行映射,太多的话将会占用太多的内存使用空间 * 此功能适用于像字典那种需要定时刷新缓存的功能 * @return */ string threshold() default ""; /** * 值的序列化方式 * @return */ class<? extends redisserializer> valueserializer() default kryoredisserializer.class; }
创建一个 aop 切面,将执行器存储到 redis
@aspect @component public class cachecustomaspect { @autowired private keygenerator keygenerator; @pointcut("@annotation(com.sanri.test.testcache.configs.cachecustom)") public void pointcut(){} public static final string invocation_cache_key_suffix = ":invocation_cache_key_suffix"; @autowired private redistemplate redistemplate; @before("pointcut()") public void registerinvoke(joinpoint joinpoint){ object[] args = joinpoint.getargs(); methodsignature methodsignature = (methodsignature) joinpoint.getsignature(); method method = methodsignature.getmethod(); object target = joinpoint.gettarget(); object cachekey = keygenerator.generate(target, method, args); string methodinvokekey = cachekey + invocation_cache_key_suffix; if(redistemplate.haskey(methodinvokekey)){ return ; } // 将方法执行器写入 redis ,然后需要刷新的时候从 redis 获取执行器,根据 cachekey ,然后刷新缓存 class<?> targetclass = aopproxyutils.ultimatetargetclass(target); methodinvoker methodinvoker = new methodinvoker(); methodinvoker.settargetclass(targetclass); methodinvoker.settargetmethod(method.getname()); methodinvoker.setarguments(args); redistemplate.setkeyserializer(new stringredisserializer()); redistemplate.setvalueserializer(new kryoredisserializer()); redistemplate.opsforvalue().set(methodinvokekey,methodinvoker); } }
重写 rediscache 的 get 方法,在获取缓存的时候查看它的过期时间,如果小于刷新阀值,则另启线程进行刷新,这里需要考虑并发问题,目前我是同步刷新的。
@override public valuewrapper get(object cachekey) { if(cachecustomoperation == null){return super.get(cachekey);} duration threshold = cachecustomoperation.getthreshold(); if(threshold == null){ // 如果不需要刷新,直接取值 return super.get(cachekey); } //判断是否需要刷新 long expire = redistemplate.getexpire(cachekey); if(expire != -2 && expire < threshold.getseconds()){ log.info("当前剩余过期时间["+expire+"]小于刷新阀值["+threshold.getseconds()+"],刷新缓存:"+cachekey+",在 cachenmae为 :"+this.getname()); synchronized (customrediscache.class) { refreshcache(cachekey.tostring(), threshold); } } return super.get(cachekey); } /** * 刷新缓存 * @param cachekey * @param threshold * @return */ private void refreshcache(string cachekey, duration threshold) { string methodinvokekey = cachekey + cachecustomaspect.invocation_cache_key_suffix; methodinvoker methodinvoker = (methodinvoker) redistemplate.opsforvalue().get(methodinvokekey); if(methodinvoker != null){ class<?> targetclass = methodinvoker.gettargetclass(); object target = aopproxyutils.getsingletontarget(applicationcontext.getbean(targetclass)); methodinvoker.settargetobject(target); try { methodinvoker.prepare(); object invoke = methodinvoker.invoke(); //然后设置进缓存和重新设置过期时间 this.put(cachekey,invoke); long ttl = threshold.tomillis(); redistemplate.expire(cachekey,ttl, timeunit.milliseconds); } catch (invocationtargetexception | illegalaccessexception | classnotfoundexception | nosuchmethodexception e) { log.error("刷新缓存失败:"+e.getmessage(),e); } } }
最后重写 rediscachemanager 把自定义的 rediscache 交由其管理
@override public cache getcache(string cachename) { cachecustomoperation cachecustomoperation = cachecustomoperationmap.get(cachename); rediscacheconfiguration rediscacheconfiguration = initialcacheconfiguration.get(cachename); if(rediscacheconfiguration == null){rediscacheconfiguration = defaultcacheconfiguration;} customrediscache customrediscache = new customrediscache(cachename,cachewriter,rediscacheconfiguration, redistemplate, applicationcontext, cachecustomoperation); customrediscache.setemptykeyexpire(this.emptykeyexpire); return customrediscache; }
说明:本文只是截取关键部分代码,完整的代码在 gitee 上
其它说明
由于 key 使用了 md5 生成,一串乱码也不知道存储的什么方法,这里提供一种解决方案,可以对有刷新时间的 key 取到其对应的方法。其实就是我在拦截器中有把当前方法的执行信息存储进 redis ,是对应那个 key 的,可以进行反序列化解析出执行类和方法信息。
一点小推广
创作不易,希望可以支持下我的开源软件,及我的小工具,欢迎来 gitee 点星,fork ,提 bug 。
excel 通用导入导出,支持 excel 公式
博客地址:
gitee:
使用模板代码 ,从数据库生成代码 ,及一些项目中经常可以用到的小工具
博客地址:
gitee:
上一篇: 猪前蹄和后蹄的区别?酱猪蹄怎么做?
下一篇: 大盘鸡咋做好吃?这么做还能在吃三碗饭!
推荐阅读
-
springboot redis-cache 自动刷新缓存
-
SpringBoot2.0实现SpringCloud config自动刷新之坑点
-
springboot使用redis对单个对象进行自动缓存更新删除的实现
-
Springboot整合Spring Cloud Kubernetes读取ConfigMap支持自动刷新配置的教程
-
IE/Firefox每次刷新时自动检查网页更新,无需手动清空缓存的设置方法
-
springboot redis-cache 自动刷新缓存
-
为何js文件后面加一个参数?这样就会自动刷新本地js文件的缓存了么?_html/css_WEB-ITnose
-
IDEA SpringBoot 热部署+html修改无需make自动刷新
-
IDEA SpringBoot 热部署+html修改无需make自动刷新
-
springboot使用redis对单个对象进行自动缓存更新删除的实现