Spring Boot Redis- Cache 设置有效时间和自动刷新缓存,注解中设置自动刷新时间

Spring Boot Redis- Cache 设置有效时间和自动刷新缓存,注解中设置自动刷新时间。

前言

这篇文章主要对小伙伴使用Spring redis-cache 进行补充,spring redis-cache是一个通用的解决方案, 虽然Spring redis-cache已经很强大了,但是面对程序员千奇百怪的想法还是有些力不从心。好了废话已经说完了,下面开始动真格的了。

问题

在我们使用spring redis-cache 做缓存的时候,往往会设置一个刷新提前量。举个栗子你设置了缓存失效期为五分钟,但是你想在最后两分钟的时候,请求到后台就刷新缓存,而且还是异步的,这样前端拿到的还是缓存的数据,但是拿完之后缓存就开始异步刷新了保证下个请求获取的是最新数据。实现这个功能,方法很多,我所见的项目组都是用代码实现的。作为极简主义者,我是看不下去了,本片文章使用sping redis-cache 的@Cacheable注解实现异步刷新redis缓存功能并且可以在注解中设置刷新时间 @Cacheable(value=“kkkk#60”,key="#map")。网上很多帖子适用版本都比较单一本片适用所有spring-data-redis 版本。

先看看效果

// 刷新时间60秒
@Cacheable(value="resultMap#60",key="#params.toString())
public Map<String,Object> getName(Map<String,String params){
	Map<String,Object> resultMap = new HashMap<>();
	......
	......
	......
	return resultMap;
}

配置redisConfig

这个文件主要对redis做一些配置如序列化方式和连接池,及缓存失效时间。

@EnableCaching //开启缓存
@Configuration
@ConditionalOnClass(RedisOperations.class)
public class RedisConfig{
	@Bean
	public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
		RedisTemplate<String,Object> template = new RedisTemplate<>();
		//配置连接工厂
		template.setConnectionFactoty(factory);
		//序列化和反序列化redis value值(默认使用JDK序列化)
		Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);
		ObjectMapper omm = new ObjectMapper();
		//指定序列化域
		omm.setVisibility(PropertyAccessor.All,JsonAutoDetect.Visibility.ANY);
		//指定序列化输入类型
		omm.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
		jacksonSeial.setObjectMapper(omm);
		
		//值采用json序列化
		template.setValueSerializer(jacksonSeial);
		//使用 StringRedisSerializer来序列化和反序列化redis的key值
		template.setKeySerializer(new StringRedisSerializer());
		
		//设置hash key 和value序列化模式
		template.setHashKeySerializer(new StringRedisSerializer());
		template.setHashValueSerializer(jacksonSeial);
		template.afterPropertiesSet();
		return template;
		
	}
	/**
     * json序列化
     * @return
     */
    @Bean
    public RedisSerializer<Object> jackson2JsonRedisSerializer() {
        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<Object>(Object.class);

        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(mapper);
        return serializer;
    }

    /**
     * 配置缓存管理器
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        // 生成一个默认配置,通过config对象即可对缓存进行自定义配置
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        // 设置缓存的默认过期时间,也是使用Duration设置
        config = config.entryTtl(Duration.ofMinutes(1))
                // 设置 key为string序列化
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                // 设置value为json序列化
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer()))
                // 不缓存空值
                .disableCachingNullValues();

        // 设置一个初始化的缓存空间set集合
        Set<String> cacheNames = new HashSet<>();
        cacheNames.add("timeGroup");
        cacheNames.add("user");

        // 对每个缓存空间应用不同的配置
        Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
        configMap.put("timeGroup", config);
        configMap.put("user", config.entryTtl(Duration.ofSeconds(120)));

        // 使用自定义的缓存配置初始化一个cacheManager
        RedisCacheManager cacheManager = RedisCacheManager.builder(RedisCacheWriter.lockingRedisCacheWriter(redisConnectionFactory))
                // 一定要先调用该方法设置初始化的缓存名,再初始化相关的配置
                .initialCacheNames(cacheNames)
                .withInitialCacheConfigurations(configMap)
                .cacheDefaults(config).transactionAware()
                .build();
        return cacheManager;
    }

    /**
     * 缓存的key是 包名+方法名+参数列表
     */
    @Bean
    public KeyGenerator keyGenerator() {
        return (target, method, objects) -> {
            StringBuilder sb = new StringBuilder();
            sb.append(target.getClass().getName());
            sb.append("::" + method.getName() + ":");
            for (Object obj : objects) {
                sb.append(obj.toString());
            }
            return sb.toString();
        };
    }

CacheAspect.java

请求进来,在方法上面扫描@Cacheable注解,会触发CacheAspect缓存的拦截器,对注解进行解析、取值。异步刷新。

@Order(0) //优先级,值越低优先级越高
@Aspect //切面
@Component
public class CacheAspect {
	private static final Logger LOGGER = LoggerFactory.getLogger(CacheAspect.class);
	@Autowired
	private CacheSupport cacheSupport;
	@Autowired
	private RedisTemplate redisTemplate;
	@Autowired
	private AsynchronousFunction asynchronousFunction;

	@Pointcut("@annotation(org.springframework.cache.annotation.Cacheable")
	public void pointCut(){}
	@Around("pointCut()")
	public Object registerInvoke(ProceedingJoinPoint joinPoint) throws Throwable{
		Object[] args = joinPoint.getArgs();
		MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
		Method method = methodSignature.getMethod();
		Object target = joinPoint.getTarget();
		Method methodJ	= this.getSpecificmethod(joinPoint);
		//解析 Cacheable
		List<Cacheable> annotations = this.getMethodAnnotations(methodJ,Cacheable.class);
		Set<String> cacheSet = new HashSet<>();
		String cacheKeyJ = null;
		for(Cacheable cacheables : annotations){
			cacheSet.addAll(Arrays.asList(cacheables.value()));
			cacheKeyJ = cacheables.key();
		}
		if(joinPoint.getSignature() instanceof MethodSignature){
			Class[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getParameterTypes,joinPoint.getArgs(),cacheSet,cacheKeyJ);
			cacheKeyJ = key.toString();
		}
		Iterator<String> it = cacheSet.iterator();
		String valueKey = null;
		while(it.hasNext()){
			valueKey = it.next();
		}
		//获取过期阀值
		String[] split = valueKey.split("#");
		long threshold = Long.parseLong(split[1]);
		String methodInvokeKey = valueKey+"::"+cacheKeyJ;
		//将方法执行器写入 redis ,然后需要刷新的时候从redis获取执行器,根据cacheKey,然后刷新缓存
		Class<?> targetClass = AopProxyUtils.ultimateTargetClass(target);
		MethodInvoker methodInvoker = new MethodInvoker();
		methodInvoker.setTargetClass(targetClass);
		methodInvoker.setTargetMethod(method.getName());
		methodInvoker.setArguments(args);

		Long expire = redisTemplate.opsForValue().getOperations().getExpire(methodInvokeKey);
		if((expire != -1 && expire != -2) && expire <= threshold){
			logger.info("开始异步刷新 redis 缓存!");
			asynchronousFunction.refreshCache(methodInvokeKey,methodInvoker);
		}
		Object proceed = joinPoint.proceed();
		return proceed;
	}
	private <T extends Annotation> List<T> getMethodAnnotations(AnnotatedElement ae, Class<T> annotationType) {
        List<T> anns = new ArrayList<T>(2);
        // look for raw annotation
        T ann = ae.getAnnotation(annotationType);
        if (ann != null) {
            anns.add(ann);
        }
        // look for meta-annotations
        for (Annotation metaAnn : ae.getAnnotations()) {
            ann = metaAnn.annotationType().getAnnotation(annotationType);
            if (ann != null) {
                anns.add(ann);
            }
        }
        return (anns.isEmpty() ? null : anns);
    }

    private Method getSpecificmethod(ProceedingJoinPoint pjp) {
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        Method method = methodSignature.getMethod();
        // The method may be on an interface, but we need attributes from the
        // target class. If the target class is null, the method will be
        // unchanged.
        Class<?> targetClass = AopProxyUtils.ultimateTargetClass(pjp.getTarget());
        if (targetClass == null && pjp.getTarget() != null) {
            targetClass = pjp.getTarget().getClass();
        }
        Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
        // If we are dealing with method with generic parameters, find the
        // original method.
        specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
        return specificMethod;
    }
		
	

AsynchronousFunction.java

一个异步更新缓存的方法。

@Component
public class AsychronousFunction{
	private static final Logger logger = LoggerFactory.getLogger(AsychronousFunction.class);
	@Autowired
	private ApplicationContext applicationContext;
	private final long EXPIRE_SECONDS = 5;
	@Autowired
	private DistributedLock distributeLock;
	private String lockUuid = "kshdfsjdfhsfs43hhj34khj33jkj_";
	/**
	 *刷新缓存
	 *@param cacheKey
	 *@Param methodInvoker
	 *return
	**/
	@Async
	public void refreshCache(String cacheKey,MethodInvoker methodInvoker){
		String methodInvokeKey = cacheKey;
		if(methodInvoker != null){
			Class<?> targetClass = methodInvoker.getTargetClass();
			Object target = AopProxyUtils.getSingletonTarget(applicationContext.getBean(targetClass));
			methodInvoker.setTargetObject(target);
			//枷锁(分布式锁)
			if(!distributedLock.getLock(DistributedLock.REDIS_LOCK_KEY+lockUuid,DistributedLock.REDIS_LOCK_KEY+lockUuid,DistributedLock.REDIS_LOCK_TIME) == false){
				try{
					methodInvoker.prepare();
					Object invoke = methodInvoker.invoke();
					//然后设置进缓存和重新设置过期时间
					redisTemplate.opsForValue().set(cacheKey,invoke);
					redisTemplate.expire(cacheKey,EXPIRE_SECONDS,TimeUnit.MINUTES);
				} catch(InvocationTargetException | IllegalAccessException | ClassNotFoundException | NoSuchMethodException e){
					logger.error("刷新缓存失败:"+e.getMessage(),e);
				} finally {
					distributedLock.releaseLock(DistributedLock.REDIS_LOCK_KEY+lockUuid,DistributedLock.REDIS_LOCK_KEY+lockUuid);
				}
			}
		}
	}
}

结语

到这里就实现了对缓存@Cacheable注解的扩展,通过注解实现redis缓存异步刷新功能,下面是github项目源码。
链接: github

图片: Alt

参考文章: https://www.jianshu.com/p/275cb42080d9

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值