springboot redis-cache 自动刷新缓存
这篇文章是对上一篇 spring-data-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 公式
博客地址:https://blog.csdn.net/sanri1993/article/details/100601578
gitee:https://gitee.com/sanri/sanri-excel-poi
使用模板代码 ,从数据库生成代码 ,及一些项目中经常可以用到的小工具
博客地址:https://blog.csdn.net/sanri1993/article/details/98664034
gitee:https://gitee.com/sanri/sanri-tools-maven
springboot redis-cache 自动刷新缓存的更多相关文章
- 【Redis】SpringBoot+Redis+Ehcache实现二级缓存
一.概述 1.1 一些疑惑? 1.2 场景 1.3 一级缓存.两级缓存的产生 1.4 流程分析 二.项目搭建 一.概述 1.1 一些疑惑? Ehcache本地内存 Redis 分布式缓存可以共享 一级 ...
- SpringBoot整合Nacos自动刷新配置
目的 Nacos作为SpringBoot服务的注册中心和配置中心. 在NacosServer中修改配置文件,在SpringBoot不重启的情况下,获取到修改的内容. 本例将在配置文件中配置一个 cml ...
- SpringBoot + redis + @Cacheable注解实现缓存清除缓存
一.Application启动类添加注解 @EnableCaching 二.注入配置 @Bean public CacheManager cacheManager(RedisTemplate redi ...
- Spring Boot 揭秘与实战(二) 数据缓存篇 - Redis Cache
文章目录 1. Redis Cache 集成 2. 源代码 本文,讲解 Spring Boot 如何集成 Redis Cache,实现缓存. 在阅读「Spring Boot 揭秘与实战(二) 数据缓存 ...
- Azure Redis Cache
将于 2014 年 9 月 1 日停止Azure Shared Cache服务,因此你需要在该日期前迁移到 Azure Redis Cache.Azure Redis Cache包含以下两个层级的产品 ...
- 基于Spring Cache实现二级缓存(Caffeine+Redis)
一.聊聊什么是硬编码使用缓存? 在学习Spring Cache之前,笔者经常会硬编码的方式使用缓存. 我们来举个实际中的例子,为了提升用户信息的查询效率,我们对用户信息使用了缓存,示例代码如下: @A ...
- Spring Cache扩展:注解失效时间+主动刷新缓存(二)
*:first-child { margin-top: 0 !important; } body > *:last-child { margin-bottom: 0 !important; } ...
- Firefox每次刷新时自动清空缓存的设置方法
当我们开发网页应用时候,为了保证每次看到的页面是最新的,需要在刷新页面时清除页面缓存. 如果每次都手动清除比较麻烦,好在多数浏览器都支持自动清除缓存的功能. IE下我们可以将缓存设置为"每次 ...
- Spring Cache扩展:注解失效时间+主动刷新缓存
*:first-child { margin-top: 0 !important; } body>*:last-child { margin-bottom: 0 !important; } /* ...
随机推荐
- 041 模块5-jieba库的使用
目录 一.jieba库基本介绍 1.1 jieba库概述 1.2 jieba库的安装 1.3 jieba分词的原理 二.jieba库使用说明 2.1 jieba分词的三种模式 2.2 jieba库常用 ...
- Python学习之旅:用Python制作一个打字训练小工具
一.写在前面 说道程序员,你会想到什么呢?有人认为程序员象征着高薪,有人认为程序员都是死肥宅,还有人想到的则是996和 ICU. 别人眼中的程序员:飞快的敲击键盘.酷炫的切换屏幕.各种看不懂的字符代码 ...
- C#将数据导入到excel文件
最近在做C#对excel的操作程序,简单的与datagridview的交互如下 using System;using System.Collections.Generic;using System.C ...
- Kubernetes v1.16 发布 | 云原生生态周报 Vol. 20
作者:心贵.进超.元毅.心水.衷源.洗兵 业界要闻 Kubernetes v1.16 发布 在这次发布中值得关注的一些特性和 Feature: CRD 正式进入 GA 阶段: Admission We ...
- 机器学习常用性能度量中的Accuracy、Precision、Recall、ROC、F score等都是些什么东西?
一篇文章就搞懂啦,这个必须收藏! 我们以图片分类来举例,当然换成文本.语音等也是一样的. Positive 正样本.比如你要识别一组图片是不是猫,那么你预测某张图片是猫,这张图片就被预测成了正样本. ...
- Mac 安装python 3.*新版本的详细步骤
Mac 系统自带python,不过自带的python版本都是2.*版本.虽然不影响老版本项目的运行, 但是python最新的3.*版本的一些语法与2.*版本并不相同,我们不论是学习还是使用,当然用最新 ...
- (转)在阿里云 CentOS 服务器(ECS)上搭建 nginx + mysql + php-fpm 环境
阿里云的云服务器(ECS)可以选择多种操作系统,打算用它运行 Drupal或者 WordPress ,你最好选择 Linux 系统,这篇文章的演示是基于阿里云的 CentOS 操作系统的服务器.我们在 ...
- 【linux】【elasticsearch】解决docker pull error pulling image configuration: Get https://d2iks1dkcwqcbx.cloudfront.net/
网络原因导致的问题: error pulling image configuration: Get https://d2iks1dkcwqcbx.cloudfront.net/docker/regis ...
- 【linux】【sonarqube】安装sonarqube7.9
前言 SonarQube 是一款用于代码质量管理的开源工具,它主要用于管理源代码的质量. 通过插件形式,可以支持众多计算机语言,比如 java, C#, go,C/C++, PL/SQL, Cobol ...
- 品Spring:帝国的基石
序 生活是一杯酒,有时需要麻醉自己,才能够暂时忘却痛苦与不快.生活是一杯茶,有时需要细细品味,才发现苦涩背后也会有甘甜. Spring是一杯酒,一眼望不到边的官方文档,着实让人难以下咽.Spring是 ...