Spring Boot Cache Actual Redis sets the effective time and automatically refreshes the cache, time support is configured in the configuration file

Keywords: Programming Redis Spring Attribute less

Problem description

The @Cacheable annotation provided by Spring Cache does not support configuring expiration times and automatic refresh of caches. We can configure the default expiration time by configuring CacheManneg and configure the expiration time separately for each cache container (value), but it always feels less flexible. Here is an example:

@Bean
public CacheManager cacheManager(RedisTemplate redisTemplate) {
    RedisCacheManager cacheManager= new RedisCacheManager(redisTemplate);
    cacheManager.setDefaultExpiration(60);
    Map<String,Long> expiresMap=new HashMap<>();
    expiresMap.put("Product",5L);
    cacheManager.setExpires(expiresMap);
    return cacheManager;
}

We want to configure the expiration time and automatic refresh time directly on the annotations, just like this:

@Cacheable(value = "people#120#90", key = "#person.id")
public Person findOne(Person person) {
    Person p = personRepository.findOne(person.getId());
    System.out.println("by id,key by:" + p.getId() + "Data is cached");
    return p;
}

value attributes are separated by #. The first is the name of the original cache container, the second is the effective time of the cache, and the third is the automatic refresh time of the cache in seconds.

The effective time and auto refresh time of the cache support the SpEl expression and support the configuration in the configuration file, such as:

@Cacheable(value = "people#${select.cache.timeout:1800}#${select.cache.refresh:600}", key = "#person.id", sync = true)//3
public Person findOne(Person person) {
    Person p = personRepository.findOne(person.getId());
    System.out.println("by id,key by:" + p.getId() + "Data is cached");
    return p;
}

Solutions

Looking at the source code, you will find that the top-level interface for caching is the CacheManager and Cache interfaces.

Cache Manager Description

The Cache Manager function is actually very simple to manage the cache. There are only two methods for the interface to get a Cache according to the container name. There is also the return of all cache names.

public interface CacheManager {

	/**
	 * Get a Cache by name (in the implementation class, if there is a Cache, it returns, and if not, it creates a new Cache and puts it in the Map container)
	 * @param name the cache identifier (must not be {@code null})
	 * @return the associated cache, or {@code null} if none found
	 */
	Cache getCache(String name);

	/**
	 * Returns a collection of cache names
	 * @return the names of all caches known by the cache manager
	 */
	Collection<String> getCacheNames();

}

Cache description

Cache interface mainly operates on caching. get gets the value from the cache server according to the cache key, put puts the data into the cache server according to the cache key, and evict deletes the data from the cache according to the key.

public interface Cache {

	ValueWrapper get(Object key);

	void put(Object key, Object value);

	void evict(Object key);

	...
}

Request step

  1. When the request comes in and the @Cacheable annotation is scanned on the method side, the interceptor for the cache of org.spring framework.cache.interceptor.CacheInterceptor will be triggered.
  2. The getCache method of the Cache Manager is then called to get the Cache, and if there is no (first visit) a new Cache is created and returned.
  3. The get method is called according to the Cache obtained to get the value in the cache. There is a bug in RedisCache. The source code first determines whether the key exists, then caches to get the value. There is a bug in high concurrency.

code analysis

At the top, we said that Spring Cache can configure expiration time by configuring Cache Manager. So where is the expiration time used? Set the default time set DefaultExpiration, set the valid time set Expires according to the specific name, and get a valid time computeExpiration of the cache name (value attribute). The real use of the valid time is in the createCache method, which is called in the getCache method of the parent class. From the Redis CacheManager source code, we can see that:

// Set the default time
public void setDefaultExpiration(long defaultExpireTime) {
	this.defaultExpiration = defaultExpireTime;
}

// Set valid time by specific name
public void setExpires(Map<String, Long> expires) {
	this.expires = (expires != null ? new ConcurrentHashMap<String, Long>(expires) : null);
}
// Get a key's valid time
protected long computeExpiration(String name) {
	Long expiration = null;
	if (expires != null) {
		expiration = expires.get(name);
	}
	return (expiration != null ? expiration.longValue() : defaultExpiration);
}

@SuppressWarnings("unchecked")
protected RedisCache createCache(String cacheName) {
    // Called the above method to get the valid time of the cache name
	long expiration = computeExpiration(cacheName);
	// Cache objects are created and this valid time is used
	return new RedisCache(cacheName, (usePrefix ? cachePrefix.prefix(cacheName) : null), redisOperations, expiration,
			cacheNullValues);
}

// Rewrite the getMissingCache of the parent class. To create Cache
@Override
protected Cache getMissingCache(String name) {
	return this.dynamic ? createCache(name) : null;
}

AbstractCacheManager parent source code:

// If the getMissingCache method is not called, a new Cache is generated and placed in the Map container.
@Override
public Cache getCache(String name) {
	Cache cache = this.cacheMap.get(name);
	if (cache != null) {
		return cache;
	}
	else {
		// Fully synchronize now for missing cache creation...
		synchronized (this.cacheMap) {
			cache = this.cacheMap.get(name);
			if (cache == null) {
			    // If Cache is not found to call this method, the default return value of this method, NULL, is implemented by the subclass itself. The above is how the subclass implements itself.
				cache = getMissingCache(name);
				if (cache != null) {
					cache = decorateCache(cache);
					this.cacheMap.put(name, cache);
					updateCacheNames(name);
				}
			}
			return cache;
		}
	}
}

The key to setting this valid time is the getCache method, where the name parameter is the value attribute on our annotations. So parsing the name of this particular format here gives me the expiration time and refresh time of the configuration. The getMissingCache method sets the expiration time when a new cache is created. The generated Cache object operates on the cache with the expiration time of our configuration, and then the expiration takes effect. The time to parse the SpEL expression to get the configuration file is also completed step by step.

CustomizedRedisCacheManager source code:

package com.xiaolyuh.redis.cache;

import com.xiaolyuh.redis.cache.helper.SpringContextHolder;
import com.xiaolyuh.redis.utils.ReflectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.cache.Cache;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.core.RedisOperations;

import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Customized redis cache manager
 * Configure expiration time on support methods
 * Supporting Hot Load Caching: Actively refresh the cache when it is about to expire
 *
 * @author yuhao.wang
 */
public class CustomizedRedisCacheManager extends RedisCacheManager {

    private static final Logger logger = LoggerFactory.getLogger(CustomizedRedisCacheManager.class);

    /**
     * Parent cacheMap field
     */
    private static final String SUPER_FIELD_CACHEMAP = "cacheMap";

    /**
     * Parent dynamic field
     */
    private static final String SUPER_FIELD_DYNAMIC = "dynamic";

    /**
     * Parent cacheNullValues field
     */
    private static final String SUPER_FIELD_CACHENULLVALUES = "cacheNullValues";

    /**
     * Parent updateCacheNames method
     */
    private static final String SUPER_METHOD_UPDATECACHENAMES = "updateCacheNames";

    /**
     * Separator for Cache Parameters
     * Array element 0 = the name of the cache
     * Array element 1 = cache expiration time TTL
     * Array Element 2 = How many seconds does the cache begin to actively fail to force refresh
     */
    private static final String SEPARATOR = "#";

    /**
     * SpEL Identifier
     */
    private static final String MARK = "$";

    RedisCacheManager redisCacheManager = null;

    @Autowired
    DefaultListableBeanFactory beanFactory;

    public CustomizedRedisCacheManager(RedisOperations redisOperations) {
        super(redisOperations);
    }

    public CustomizedRedisCacheManager(RedisOperations redisOperations, Collection<String> cacheNames) {
        super(redisOperations, cacheNames);
    }

    public RedisCacheManager getInstance() {
        if (redisCacheManager == null) {
            redisCacheManager = SpringContextHolder.getBean(RedisCacheManager.class);
        }
        return redisCacheManager;
    }

    @Override
    public Cache getCache(String name) {
        String[] cacheParams = name.split(SEPARATOR);
        String cacheName = cacheParams[0];

        if (StringUtils.isBlank(cacheName)) {
            return null;
        }

        // Valid time, initialization to get default valid time
        Long expirationSecondTime = getExpirationSecondTime(cacheName, cacheParams);
        // Automatic refresh time, default is 0
        Long preloadSecondTime = getExpirationSecondTime(cacheParams);

        // Retrieving Cached Container Objects from the Parent Class by Reflection
        Object object = ReflectionUtils.getFieldValue(getInstance(), SUPER_FIELD_CACHEMAP);
        if (object != null && object instanceof ConcurrentHashMap) {
            ConcurrentHashMap<String, Cache> cacheMap = (ConcurrentHashMap<String, Cache>) object;
            // Generate Cache objects and save them in the Cache container of the parent class
            return getCache(cacheName, expirationSecondTime, preloadSecondTime, cacheMap);
        } else {
            return super.getCache(cacheName);
        }

    }

    /**
     * Get expiration time
     *
     * @return
     */
    private long getExpirationSecondTime(String cacheName, String[] cacheParams) {
        // Valid time, initialization to get default valid time
        Long expirationSecondTime = this.computeExpiration(cacheName);

        // Set the key valid time
        if (cacheParams.length > 1) {
            String expirationStr = cacheParams[1];
            if (!StringUtils.isEmpty(expirationStr)) {
                // Support configuration expiration time reading configuration file time using EL expression
                if (expirationStr.contains(MARK)) {
                    expirationStr = beanFactory.resolveEmbeddedValue(expirationStr);
                }
                expirationSecondTime = Long.parseLong(expirationStr);
            }
        }

        return expirationSecondTime;
    }

    /**
     * Get automatic refresh time
     *
     * @return
     */
    private long getExpirationSecondTime(String[] cacheParams) {
        // Automatic refresh time, default is 0
        Long preloadSecondTime = 0L;
        // Setting automatic refresh time
        if (cacheParams.length > 2) {
            String preloadStr = cacheParams[2];
            if (!StringUtils.isEmpty(preloadStr)) {
                // Support configuration refresh time read configuration file time using EL expression
                if (preloadStr.contains(MARK)) {
                    preloadStr = beanFactory.resolveEmbeddedValue(preloadStr);
                }
                preloadSecondTime = Long.parseLong(preloadStr);
            }
        }
        return preloadSecondTime;
    }

    /**
     * Rewrite the getCache method of the parent class with three parameters
     *
     * @param cacheName            Cache name
     * @param expirationSecondTime Expiration date
     * @param preloadSecondTime    Automatic refresh time
     * @param cacheMap             cacheMap object of parent class obtained by reflection
     * @return Cache
     */
    public Cache getCache(String cacheName, long expirationSecondTime, long preloadSecondTime, ConcurrentHashMap<String, Cache> cacheMap) {
        Cache cache = cacheMap.get(cacheName);
        if (cache != null) {
            return cache;
        } else {
            // Fully synchronize now for missing cache creation...
            synchronized (cacheMap) {
                cache = cacheMap.get(cacheName);
                if (cache == null) {
                    // Call our own getMissingCache method to create our own cache
                    cache = getMissingCache(cacheName, expirationSecondTime, preloadSecondTime);
                    if (cache != null) {
                        cache = decorateCache(cache);
                        cacheMap.put(cacheName, cache);

                        // Reflect the updateCacheNames(cacheName) method to execute the parent class
                        Class<?>[] parameterTypes = {String.class};
                        Object[] parameters = {cacheName};
                        ReflectionUtils.invokeMethod(getInstance(), SUPER_METHOD_UPDATECACHENAMES, parameterTypes, parameters);
                    }
                }
                return cache;
            }
        }
    }

    /**
     * Create cache
     *
     * @param cacheName            Cache name
     * @param expirationSecondTime Expiration date
     * @param preloadSecondTime    Brake refresh time
     * @return
     */
    public CustomizedRedisCache getMissingCache(String cacheName, long expirationSecondTime, long preloadSecondTime) {

        logger.info("cache cacheName: {},Expiration date:{}, Automatic refresh time:{}", cacheName, expirationSecondTime, preloadSecondTime);
        Boolean dynamic = (Boolean) ReflectionUtils.getFieldValue(getInstance(), SUPER_FIELD_DYNAMIC);
        Boolean cacheNullValues = (Boolean) ReflectionUtils.getFieldValue(getInstance(), SUPER_FIELD_CACHENULLVALUES);
        return dynamic ? new CustomizedRedisCache(cacheName, (this.isUsePrefix() ? this.getCachePrefix().prefix(cacheName) : null),
                this.getRedisOperations(), expirationSecondTime, preloadSecondTime, cacheNullValues) : null;
    }
}

What about the automatic refresh time?

There is no refresh time in the RedisCache attribute, so we need to add an additional attribute preloadSecondTime to store the refresh time when we inherit this class to rewrite our own Cache. The value is specified when the getMissingCache method creates the Cache object.

CustomizedRedisCache partial source code:

/**
 * The time when the cache actively refreshes the cache before it fails
 * Unit: sec.
 */
private long preloadSecondTime = 0;

// Rewritten Construction Method
public CustomizedRedisCache(String name, byte[] prefix, RedisOperations<? extends Object, ? extends Object> redisOperations, long expiration, long preloadSecondTime) {
    super(name, prefix, redisOperations, expiration);
    this.redisOperations = redisOperations;
    // Specify automatic refresh time
    this.preloadSecondTime = preloadSecondTime;
    this.prefix = prefix;
}

// Rewritten Construction Method
public CustomizedRedisCache(String name, byte[] prefix, RedisOperations<? extends Object, ? extends Object> redisOperations, long expiration, long preloadSecondTime, boolean allowNullValues) {
    super(name, prefix, redisOperations, expiration, allowNullValues);
    this.redisOperations = redisOperations;
    // Specify automatic refresh time
    this.preloadSecondTime = preloadSecondTime;
    this.prefix = prefix;
}

Now that the automatic refresh time is available, how can he refresh it automatically?

When we call Cache's get method, we all go to the cache service to query the cache. At this time, we look up the valid time of one more cache and compare it with the automatic refresh time we configured. If the valid time of the cache is less than the automatic refresh time, we refresh the cache. (Note here that under high concurrency, I will refresh the cache.) We'd better put only one request to refresh the data to minimize the pressure on the data, so add a distributed lock to this location. So let's rewrite the get method.

CustomizedRedisCache partial source code:

/**
 * Rewrite the get method to retrieve the remaining time of the cache after it is cached, and refresh the cache manually if the time is less than the refresh time we configure.
 * In order not to affect the performance of get, the background thread is enabled to complete the cache brush.
 * And only one thread is put to refresh the data.
 *
 * @param key
 * @return
 */
@Override
public ValueWrapper get(final Object key) {
    RedisCacheKey cacheKey = getRedisCacheKey(key);
    String cacheKeyStr = new String(cacheKey.getKeyBytes());
    // Call the rewritten get method
    ValueWrapper valueWrapper = this.get(cacheKey);

    if (null != valueWrapper) {
        // Refresh Cached Data
        refreshCache(key, cacheKeyStr);
    }
    return valueWrapper;
}

/**
 * Rewrite the get function of the parent class.
 * The get method of the parent class first uses exists to determine whether the key exists or not, does not return null, exists and then goes to the redis cache to get the value. This can lead to concurrency problems.
 * If a request calls the exists function to determine that the key exists, but at the next moment the cache expires or is deleted.
 * When you go back to the cache to get the value, null will be returned.
 * You can get the cached value first, and then determine whether the key exists.
 *
 * @param cacheKey
 * @return
 */
@Override
public RedisCacheElement get(final RedisCacheKey cacheKey) {

    Assert.notNull(cacheKey, "CacheKey must not be null!");

    // Getting cached values based on key
    RedisCacheElement redisCacheElement = new RedisCacheElement(cacheKey, fromStoreValue(lookup(cacheKey)));
    // Judging whether key exists
    Boolean exists = (Boolean) redisOperations.execute(new RedisCallback<Boolean>() {

        @Override
        public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
            return connection.exists(cacheKey.getKeyBytes());
        }
    });

    if (!exists.booleanValue()) {
        return null;
    }

    return redisCacheElement;
}

/**
 * Refresh cached data
 */
private void refreshCache(Object key, String cacheKeyStr) {
    Long ttl = this.redisOperations.getExpire(cacheKeyStr);
    if (null != ttl && ttl <= CustomizedRedisCache.this.preloadSecondTime) {
        // Open threads as little as possible, because the thread pool is limited
        ThreadTaskHelper.run(new Runnable() {
            @Override
            public void run() {
                // Add a distributed lock and place only one request to refresh the cache
                RedisLock redisLock = new RedisLock((RedisTemplate) redisOperations, cacheKeyStr + "_lock");
                try {
                    if (redisLock.lock()) {
                        // After obtaining the lock, determine the expiration time to see if the data needs to be loaded
                        Long ttl = CustomizedRedisCache.this.redisOperations.getExpire(cacheKeyStr);
                        if (null != ttl && ttl <= CustomizedRedisCache.this.preloadSecondTime) {
                            // Reloading cached data by obtaining proxy method information
                            CustomizedRedisCache.this.getCacheSupport().refreshCacheByKey(CustomizedRedisCache.super.getName(), key.toString());
                        }
                    }
                } catch (Exception e) {
                    logger.info(e.getMessage(), e);
                } finally {
                    redisLock.unlock();
                }
            }
        });
    }
}

Then automatic refresh must use the method to access the database, get the value and refresh the cache. How can we call a method at this point?

We use java's reflection mechanism. So we use a container to store the method information of caching methods, including objects, method names, parameters and so on. We created the CachedInvocation class to store this information, and then maintained this class of objects in the container.

CachedInvocation source code:

public final class CachedInvocation {

    private Object key;
    private final Object targetBean;
    private final Method targetMethod;
    private Object[] arguments;

    public CachedInvocation(Object key, Object targetBean, Method targetMethod, Object[] arguments) {
        this.key = key;
        this.targetBean = targetBean;
        this.targetMethod = targetMethod;
        if (arguments != null && arguments.length != 0) {
            this.arguments = Arrays.copyOf(arguments, arguments.length);
        }
    }

    public Object[] getArguments() {
        return arguments;
    }

    public Object getTargetBean() {
        return targetBean;
    }

    public Method getTargetMethod() {
        return targetMethod;
    }

    public Object getKey() {
        return key;
    }

    /**
     * You must override equals and hashCode methods, or you cannot override them in set sets.
     * @param o
     * @return
     */
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        CachedInvocation that = (CachedInvocation) o;

        return key.equals(that.key);
    }

    @Override
    public int hashCode() {
        return key.hashCode();
    }
}

(Scheme 1) Maintaining a container for caching method information (building a MAP in memory) and refreshing the cached CacheSupportImpl-like key code:

private final String SEPARATOR = "#";

/**
 * A container that records information about cache execution methods.
 * If there is a lot of useless cached data, it may be a memory overflow.
 */
private Map<String, Set<CachedInvocation>> cacheToInvocationsMap = new ConcurrentHashMap<>();

@Autowired
private CacheManager cacheManager;

// Refresh cache
private void refreshCache(CachedInvocation invocation, String cacheName) {

	boolean invocationSuccess;
	Object computed = null;
	try {
		// Call the method through the proxy and record the return value
		computed = invoke(invocation);
		invocationSuccess = true;
	} catch (Exception ex) {
		invocationSuccess = false;
	}
	if (invocationSuccess) {
		if (!CollectionUtils.isEmpty(cacheToInvocationsMap.get(cacheName))) {
			// Getting cache Objects for Operating cache through cacheManager
			Cache cache = cacheManager.getCache(cacheName);
			// Update Cache Objects
			cache.put(invocation.getKey(), computed);
		}
	}
}

private Object invoke(CachedInvocation invocation)
		throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {

	final MethodInvoker invoker = new MethodInvoker();
	invoker.setTargetObject(invocation.getTargetBean());
	invoker.setArguments(invocation.getArguments());
	invoker.setTargetMethod(invocation.getTargetMethod().getName());
	invoker.prepare();

	return invoker.invoke();
}

// Execution class information for registered caching methods
@Override
public void registerInvocation(Object targetBean, Method targetMethod, Object[] arguments,
		Set<String> annotatedCacheNames, String cacheKey) {

	// Get the true value on the annotation
	Collection<String> cacheNames = generateValue(annotatedCacheNames);

	// Get the key attribute value on the annotation
	Class<?> targetClass = getTargetClass(targetBean);
	Collection<? extends Cache> caches = getCache(cacheNames);
	Object key = generateKey(caches, cacheKey, targetMethod, arguments, targetBean, targetClass,
			CacheOperationExpressionEvaluator.NO_RESULT);

	// Create a new proxy object (recording method class information for caching annotations)
	final CachedInvocation invocation = new CachedInvocation(key, targetBean, targetMethod, arguments);
	for (final String cacheName : cacheNames) {
		if (!cacheToInvocationsMap.containsKey(cacheName)) {
			cacheToInvocationsMap.put(cacheName, new CopyOnWriteArraySet<>());
		}
		cacheToInvocationsMap.get(cacheName).add(invocation);
	}
}

@Override
public void refreshCache(String cacheName) {
	this.refreshCacheByKey(cacheName, null);
}


// Refresh specific key cache
@Override
public void refreshCacheByKey(String cacheName, String cacheKey) {
	// If the set collection of the proxy information class is not found according to the cache name, the refresh operation is not performed.
	// Only wait until the cache is valid, then go to the section and register the proxy method information here.
	if (!CollectionUtils.isEmpty(cacheToInvocationsMap.get(cacheName))) {
		for (final CachedInvocation invocation : cacheToInvocationsMap.get(cacheName)) {
			if (!StringUtils.isBlank(cacheKey) && invocation.getKey().toString().equals(cacheKey)) {
				logger.info("Cache:{}-{},Reload data", cacheName, cacheKey.getBytes());
				refreshCache(invocation, cacheName);
			}
		}
	}
}

(Solution 2) Maintain the container of caching method information (put it in Redis) and paste this part of the code to see the source code directly.

Now that both refresh cache and register cache execution method information are available, how can we register this execution method information into the container? There are fewer triggers.

So we also need a facet. When we execute the @Cacheable annotation to get the cache information, we also need to register the information of the execution method. So we wrote a facet:

/**
 * Cache interception for registering method information
 * @author yuhao.wang
 */
@Aspect
@Component
public class CachingAnnotationsAspect {

    private static final Logger logger = LoggerFactory.getLogger(CachingAnnotationsAspect.class);

    @Autowired
    private InvocationRegistry cacheRefreshSupport;

    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;
    }

    @Pointcut("@annotation(org.springframework.cache.annotation.Cacheable)")
    public void pointcut() {
    }

    @Around("pointcut()")
    public Object registerInvocation(ProceedingJoinPoint joinPoint) throws Throwable {

        Method method = this.getSpecificmethod(joinPoint);

        List<Cacheable> annotations = this.getMethodAnnotations(method, Cacheable.class);

        Set<String> cacheSet = new HashSet<String>();
        String cacheKey = null;
        for (Cacheable cacheables : annotations) {
            cacheSet.addAll(Arrays.asList(cacheables.value()));
            cacheKey = cacheables.key();
        }
        cacheRefreshSupport.registerInvocation(joinPoint.getTarget(), method, joinPoint.getArgs(), cacheSet, cacheKey);
        return joinPoint.proceed();

    }
}

Note: A cache name (@Cacheable's value attribute) can only be configured with one expiration time, if multiple configurations are based on the first configuration.

So far, we have achieved the complete set expiration time and refresh cache, of course, there may be some problems, I hope you can give more advice.

There is a disadvantage of using this method. We destroy the structure of Spring Cache, which leads to code changes when we switch the way of Cache.

Next article I will extend the redisCacheManager.setExpires() method to achieve expiration time and automatic refresh without breaking Spring Cache's original structure, and there will be no problem switching caches.

Code structure diagram:

Source address: https://github.com/wyh-spring-ecosystem-student/spring-boot-student/tree/releases

spring-boot-student-cache-redis project

Reference resources:

Laying-cache, a multi-level caching framework for monitoring This is an implementation of my open source multi-level caching framework. If you are interested, take a look at it.

Posted by lcoscare on Sun, 22 Sep 2019 20:36:02 -0700