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 就可以实现,下面说的是刷新缓存的实现

  1. 拦截 @Cacheable 注解,如果执行的方法是需要刷新缓存的,则注册一个 MethodInvoker 存储到 redis ,使用和存储 key 相同的键名再拼接一个后缀
  2. 当取缓存的时候,如果 key 的过期时间达到了刷新阀值,则从 redis 取到当前 cacheKey 的 MethodInvoker 然后执行方法
  3. 将上一步的值存储进缓存,并重置过期时间

引言

本文使用到的 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

原文地址:https://www.cnblogs.com/sanri1993/p/11702753.html

时间: 2024-08-03 08:30:00

springboot redis-cache 自动刷新缓存的相关文章

Azure Redis Cache

将于 2014 年 9 月 1 日停止Azure Shared Cache服务,因此你需要在该日期前迁移到 Azure Redis Cache.Azure Redis Cache包含以下两个层级的产品. 基本版 – 单节点,多规格. 标准版 – 主/从双节点,多规格.标准层产品将具有 99.9% 的 SLA. 具体文档参看 http://azure.microsoft.com/zh-cn/documentation/articles/cache-dotnet-how-to-use-azure-r

springboot redis 缓存对象

只要加入spring-boot-starter-data-redis , springboot 会自动识别并使用redis作为缓存容器,使用方式如下 gradle加入依赖 compile("org.springframework.boot:spring-boot-starter-data-redis:${springBootVersion}") redis configuration 中启用缓存 @Configuration @EnableCaching public class Re

Spring Cache扩展:注解失效时间+主动刷新缓存

*:first-child { margin-top: 0 !important; } body>*:last-child { margin-bottom: 0 !important; } /* BLOCKS =============================================================================*/ p, blockquote, ul, ol, dl, table, pre { margin: 15px 0; } /* HEAD

springboot+redis+Interceptor+自定义annotation实现接口自动幂等

前言: 在实际的开发项目中,一个对外暴露的接口往往会面临很多次请求,我们来解释一下幂等的概念:任意多次执行所产生的影响均与一次执行的影响相同.按照这个含义,最终的含义就是 对数据库的影响只能是一次性的,不能重复处理.如何保证其幂等性,通常有以下手段: 1:数据库建立唯一性索引,可以保证最终插入数据库的只有一条数据 2:token机制,每次接口请求前先获取一个token,然后再下次请求的时候在请求的header体中加上这个token,后台进行验证,如果验证通过删除token,下次请求再次判断tok

response对象处理HTTP文件头(禁用缓存、设置页面自动刷新、定时跳转网页)

response对象处理HTTP文件头 制作人:全心全意 禁用缓存 在默认情况下,浏览器将会对显示的网页内容进行缓存.这样,当用户再次访问相关网页时,浏览器会判断网页是否有变化,如果没有变化则直接显示缓存中的内容,这样可以提高网页的显示速度.对于一些安全性要求较高的网站,通常需要禁用缓存. 通过设置HTTP头的方法实现禁用缓存: <% response.setHeader("Cache-Control", "no-store"); response.setDa

SpringBoot+Redis形成数据 缓存

步骤: 第一  引入redis依赖包 第二  配置application.propresties 配之类   配置redis服务器地址 第三  创建redis配置类 开启缓存配置(@enableCaching) 配置缓存管理器(cacheManger) 配置KEY生成策略 第四  仓库中直接使用@cacheable(value="") 详情参考: https://segmentfault.com/a/1190000004389938

SpringCache自定义过期时间及自动刷新

背景前提 阅读说明(十分重要) 对于Cache和SpringCache原理不太清楚的朋友,可以看我之前写的文章:Springboot中的缓存Cache和CacheManager原理介绍 能关注SpringCache,想了解过期实现和自动刷新的朋友,肯定有一定Java基础的,所以先了解我的思想,达成共识比直接看代码重要许多 你可能只需要我里面其中一个点而不是原搬照抄 我在实现过程遇到三大坑,先跟大家说下,兴许对你有帮助 坑一:自己造轮子 对SpringCache不怎么了解,直接百度缓存看到Redi

Redis for Windows(C#缓存)配置文件详解

Redis for Windows(C#缓存)配置文件详解 前言 在上一篇文章中主要介绍了Redis在Windows平台下的下载安装和简单使用http://www.cnblogs.com/aehyok/p/3478282.html.当然我也在上一篇中指定过配置文件,并且修改其中的端口port.本文将主要来探讨redis强大的配置文件. 我现在使用的redis版本为2.6.首先奉上配置文件的源文件. # Redis configuration file example # Note on unit

nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

一.概述 nop支持Redis作为缓存,Redis出众的性能在企业中得到了广泛的应用.Redis支持主从复制,HA,集群. 一般来说,只有一台Redis是不可行的,原因如下: 单台Redis服务器会发生单点故障,并且单服务器需要处理所有的请求会导致压力较大. 单台Redis服务器内存容量有限,不易扩展. 第一个问题可以通过Redis主从模式实现单节点的高可用(HA). 从节点(slave)是主节点(master)副本,当主节点(master)宕机后,Redis 哨兵(Sentinel)会自动将从