Spring boot redis cache sets the effective time and auto refresh cache, and the auto refresh time is set in the annotation

Posted by theverychap on Tue, 23 Jun 2020 11:00:59 +0200

Spring boot redis cache sets the effective time and auto refresh cache, and the auto refresh time is set in the annotation.

preface

This article mainly supplements the spring redis cache used by small partners. Spring redis cache is a general solution. Although spring redis cache has been very powerful, it still can't cope with programmers' strange ideas. All right, I've finished talking. Now I'm going to start to move.

problem

When we use spring redis cache for caching, we often set a refresh lead. For example, you set the cache expiration period to five minutes, but you want to refresh the cache in the background in the last two minutes, and it's asynchronous, so the front end gets the cached data, but after that, the cache starts to refresh asynchronously to ensure that the next request gets the latest data. There are many ways to realize this function. All the project groups I see are implemented in code. As a minimalist, I can't go any further. This article uses the @ Cacheable annotation of spinning redis cache to realize the asynchronous refresh redis cache function and can set the refresh time @ Cacheable(value = "KKK ා 60", key = "ා map") in the annotation. Many posts on the Internet are available in a single version. This film is applicable to all spring data redis versions.

Let's see the effect first

// Refresh time 60 seconds
@Cacheable(value="resultMap#60",key="#params.toString())
public Map<String,Object> getName(Map<String,String params){
	Map<String,Object> resultMap = new HashMap<>();
	......
	......
	......
	return resultMap;
}

Configure redisConfig

This file mainly configures redis, such as serialization mode, connection pool, and cache expiration time.

@EnableCaching //Open cache
@Configuration
@ConditionalOnClass(RedisOperations.class)
public class RedisConfig{
	@Bean
	public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
		RedisTemplate<String,Object> template = new RedisTemplate<>();
		//Configure connection factory
		template.setConnectionFactoty(factory);
		//Serializing and deserializing redis value values (JDK serialization is used by default)
		Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);
		ObjectMapper omm = new ObjectMapper();
		//Specify serialization domain
		omm.setVisibility(PropertyAccessor.All,JsonAutoDetect.Visibility.ANY);
		//Specify serialization input type
		omm.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
		jacksonSeial.setObjectMapper(omm);
		
		//Value is serialized in json
		template.setValueSerializer(jacksonSeial);
		//Use StringRedisSerializer to serialize and deserialize the key value of redis
		template.setKeySerializer(new StringRedisSerializer());
		
		//Set hash key and value serialization mode
		template.setHashKeySerializer(new StringRedisSerializer());
		template.setHashValueSerializer(jacksonSeial);
		template.afterPropertiesSet();
		return template;
		
	}
	/**
     * json serialize
     * @return
     */
    @Bean
    public RedisSerializer<Object> jackson2JsonRedisSerializer() {
        //Use Jackson 2jsonredisserializer to serialize and deserialize the value of redis
        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;
    }

    /**
     * Configure cache manager
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        // Generate a default configuration, and customize the cache configuration through the config object
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        // Set the default expiration time of the cache, and also use the Duration setting
        config = config.entryTtl(Duration.ofMinutes(1))
                // Set key to string serialization
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                // Set value to json serialization
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer()))
                // Do not cache null values
                .disableCachingNullValues();

        // Set an initial set of cache spaces
        Set<String> cacheNames = new HashSet<>();
        cacheNames.add("timeGroup");
        cacheNames.add("user");

        // Apply different configurations to each cache space
        Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
        configMap.put("timeGroup", config);
        configMap.put("user", config.entryTtl(Duration.ofSeconds(120)));

        // Initializing a cacheManager with a custom cache configuration
        RedisCacheManager cacheManager = RedisCacheManager.builder(RedisCacheWriter.lockingRedisCacheWriter(redisConnectionFactory))
                // First call this method to set the initialized cache name, and then initialize the related configuration
                .initialCacheNames(cacheNames)
                .withInitialCacheConfigurations(configMap)
                .cacheDefaults(config).transactionAware()
                .build();
        return cacheManager;
    }

    /**
     * The cached key is package name + method name + parameter list
     */
    @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

When the request comes in, scan @ Cacheable annotation on the method, it will trigger the blocker of cacheevent cache to parse and take value for the annotation. Asynchronous refresh.

@Order(0) //Priority, lower value, higher priority
@Aspect //section
@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);
		//Parsing 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();
		}
		//Get expiration threshold
		String[] split = valueKey.split("#");
		long threshold = Long.parseLong(split[1]);
		String methodInvokeKey = valueKey+"::"+cacheKeyJ;
		//Write the method executor to redis, then get the executor from redis when you need to refresh, and then refresh the cache according to 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("Start asynchronous refresh redis Cache!");
			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

A method to update the cache asynchronously.

@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_";
	/**
	 *Refresh cache
	 *@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);
			//Shackles (distributed locks)
			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();
					//Then set the cache and reset the expiration time
					redisTemplate.opsForValue().set(cacheKey,invoke);
					redisTemplate.expire(cacheKey,EXPIRE_SECONDS,TimeUnit.MINUTES);
				} catch(InvocationTargetException | IllegalAccessException | ClassNotFoundException | NoSuchMethodException e){
					logger.error("Refresh cache failed:"+e.getMessage(),e);
				} finally {
					distributedLock.releaseLock(DistributedLock.REDIS_LOCK_KEY+lockUuid,DistributedLock.REDIS_LOCK_KEY+lockUuid);
				}
			}
		}
	}
}

epilogue

This is the extension of cache @ Cacheable annotation. The asynchronous refresh function of redis cache is realized by annotation. Here is the source code of github project.
Link: github

Picture:

Reference article: https://www.jianshu.com/p/275cb42080d9

Topics: Redis Spring JSON Java