这篇文章是对上一篇 spring-data-redis-cache 的使用 的一个补充,上文说到 spring-data-redis-cache 虽然比较强悍,但还是有些不足的,它是一个通用的解决方案,但对于企业级的项目,住住需要解决更多的问题,常见的问题有
增强 spring-data-redis-cache 的功能,增强的功能如下
看网上大部分文章都是互相抄袭,而且都是旧版本的,有时还有错误,本文提供一个 spring-data-redis-2.0.10.RELEASE.jar 版本的解决方案。本文代码是经过测试的,但未在线上环境验证,使用时需注意可能存在 bug 。
过期时间的配置很简单,修改 initialCacheConfiguration
就可以实现,下面说的是刷新缓存的实现
@Cacheable
注解,如果执行的方法是需要刷新缓存的,则注册一个 MethodInvoker
存储到 redis ,使用和存储 key 相同的键名再拼接一个后缀 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 的,可以进行反序列化解析出执行类和方法信息。