problem
Last article Spring Boot Cache + redis sets the effective time and automatically refreshes the cache, time support is configured in the configuration file In a time-dependent manner, the Value of the annotation is expanded directly, such as:
@Override @Cacheable(value = "people#${select.cache.timeout:1800}#${select.cache.refresh:600}", key = "#person.id", sync = true) public Person findOne(Person person, String a, String[] b, List<Long> c) { Person p = personRepository.findOne(person.getId()); System.out.println("by id,key by:" + p.getId() + "Data is cached"); System.out.println(redisTemplate); return p; }
However, one drawback of this approach is that it destroys the original Spring Cache architecture, resulting in a lot of code changes if you want to change the cache later.
Solutions
RedisCache Manager can specify expiration times when configuring Cache Manager beans, such as:
@Bean public RedisCacheManager cacheManager(RedisTemplate<String, Object> redisTemplate) { RedisCacheManager redisCacheManager = new RedisCacheManager(redisTemplate); // Open the most key prefix with the cache name redisCacheManager.setUsePrefix(true); //Here you can set a default expiration time unit in seconds. redisCacheManager.setDefaultExpiration(redisDefaultExpiration); // Setting the expiration time of the cache Map<String, Long> expires = new HashMap<>(); expires.put("people", 1000); redisCacheManager.setExpires(expires); return redisCacheManager; }
Let's borrow the idea of redisCacheManager.setExpires(expires) for extension. Create a new CacheTime class directly to save expiration time and automatic refresh time.
When RedisCache Manager calls getCache(name) to get the cache, it calls getMissing Cache (String CacheName) to create a new cache when no cache is found. When creating a new cache, we can get CacheTime according to the key in the extended Map < String, CacheTime > cacheTimes to get the valid time and automatic refresh time.
Concrete realization
Let's first create a new CacheTime class
CacheTime:
/** * @author yuhao.wang */ public class CacheTime { public CacheTime(long preloadSecondTime, long expirationSecondTime) { this.preloadSecondTime = preloadSecondTime; this.expirationSecondTime = expirationSecondTime; } /** * The time when the cache actively refreshes the cache before it fails * Unit: sec. */ private long preloadSecondTime = 0; /** * Cache validity time */ private long expirationSecondTime; public long getPreloadSecondTime() { return preloadSecondTime; } public long getExpirationSecondTime() { return expirationSecondTime; } }
Extend the RedisCache class
Like the Customized RedisCache class in the previous article, the main solution is:
- Get a bug cached in large concurrency. details
- Determine the expiration time and automatic refresh time of the cache when it is retrieved, and refresh the cache according to this value
CustomizedRedisCache:
/** * Customized redis cache * * @author yuhao.wang */ public class CustomizedRedisCache extends RedisCache { private static final Logger logger = LoggerFactory.getLogger(CustomizedRedisCache.class); private CacheSupport getCacheSupport() { return SpringContextUtils.getBean(CacheSupport.class); } private final RedisOperations redisOperations; private final byte[] prefix; /** * The time when the cache actively refreshes the cache before it fails * Unit: sec. */ private long preloadSecondTime = 0; /** * Cache validity time */ private long expirationSecondTime; 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 a valid time this.expirationSecondTime = expiration; // Specify automatic refresh time this.preloadSecondTime = preloadSecondTime; this.prefix = prefix; } 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 a valid time this.expirationSecondTime = expiration; // Specify automatic refresh time this.preloadSecondTime = preloadSecondTime; this.prefix = prefix; } /** * 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 = getCacheKey(key); // 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 or not 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 thread pools are limited ThreadTaskUtils.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(), cacheKeyStr); } } } catch (Exception e) { logger.info(e.getMessage(), e); } finally { redisLock.unlock(); } } }); } } public long getExpirationSecondTime() { return expirationSecondTime; } /** * Get RedisCacheKey * * @param key * @return */ public RedisCacheKey getRedisCacheKey(Object key) { return new RedisCacheKey(key).usePrefix(this.prefix) .withKeySerializer(redisOperations.getKeySerializer()); } /** * Get RedisCacheKey * * @param key * @return */ public String getCacheKey(Object key) { return new String(getRedisCacheKey(key).getKeyBytes()); } }
Extending RedisCache Manager
When the cache is retrieved by the getCache(String name) method, the new cache is created by calling getMissing Cache (String cacheName) when the cache is not found.
CustomizedRedisCacheManager:
/** * 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 dynamic field */ private static final String SUPER_FIELD_DYNAMIC = "dynamic"; /** * Parent cacheNullValues field */ private static final String SUPER_FIELD_CACHENULLVALUES = "cacheNullValues"; RedisCacheManager redisCacheManager = null; // 0 - never expire private long defaultExpiration = 0; private Map<String, CacheTime> cacheTimes = null; public CustomizedRedisCacheManager(RedisOperations redisOperations) { super(redisOperations); } public CustomizedRedisCacheManager(RedisOperations redisOperations, Collection<String> cacheNames) { super(redisOperations, cacheNames); } public RedisCacheManager getInstance() { if (redisCacheManager == null) { redisCacheManager = SpringContextUtils.getBean(RedisCacheManager.class); } return redisCacheManager; } /** * Get expiration time * * @return */ public long getExpirationSecondTime(String name) { if (StringUtils.isEmpty(name)) { return 0; } CacheTime cacheTime = null; if (!CollectionUtils.isEmpty(cacheTimes)) { cacheTime = cacheTimes.get(name); } Long expiration = cacheTime != null ? cacheTime.getExpirationSecondTime() : defaultExpiration; return expiration < 0 ? 0 : expiration; } /** * Get automatic refresh time * * @return */ private long getPreloadSecondTime(String name) { // Automatic refresh time, default is 0 CacheTime cacheTime = null; if (!CollectionUtils.isEmpty(cacheTimes)) { cacheTime = cacheTimes.get(name); } Long preloadSecondTime = cacheTime != null ? cacheTime.getPreloadSecondTime() : 0; return preloadSecondTime < 0 ? 0 : preloadSecondTime; } /** * Create cache * * @param cacheName Cache name * @return */ public CustomizedRedisCache getMissingCache(String cacheName) { // Valid time, initialization to get default valid time Long expirationSecondTime = getExpirationSecondTime(cacheName); // Automatic refresh time, default is 0 Long preloadSecondTime = getPreloadSecondTime(cacheName); logger.info("cache cacheName: {},Expiration date:{}, Automatic refresh time:{}", cacheName, expirationSecondTime, preloadSecondTime); // Whether to create Cache at runtime Boolean dynamic = (Boolean) ReflectionUtils.getFieldValue(getInstance(), SUPER_FIELD_DYNAMIC); // Is NULL Allowed to Store 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; } /** * Set the effective time and refresh time of the cache, per second, according to the cache name * * @param cacheTimes */ public void setCacheTimess(Map<String, CacheTime> cacheTimes) { this.cacheTimes = (cacheTimes != null ? new ConcurrentHashMap<String, CacheTime>(cacheTimes) : null); } /** * Set the default past time in seconds * * @param defaultExpireTime */ @Override public void setDefaultExpiration(long defaultExpireTime) { super.setDefaultExpiration(defaultExpireTime); this.defaultExpiration = defaultExpireTime; } @Deprecated @Override public void setExpires(Map<String, Long> expires) { } }
Config Configuration of RedisCache Manager
@Bean public RedisCacheManager cacheManager(RedisTemplate<String, Object> redisTemplate) { CustomizedRedisCacheManager redisCacheManager = new CustomizedRedisCacheManager(redisTemplate); // Open the most key prefix with the cache name redisCacheManager.setUsePrefix(true); //Here you can set a default expiration time unit in seconds. redisCacheManager.setDefaultExpiration(redisDefaultExpiration); // Setting cache expiration time and automatic refresh time Map<String, CacheTime> cacheTimes = new HashMap<>(); cacheTimes.put("people", new CacheTime(selectCacheTimeout, selectCacheRefresh)); cacheTimes.put("people1", new CacheTime(120, 115)); cacheTimes.put("people2", new CacheTime(120, 115)); redisCacheManager.setCacheTimess(cacheTimes); return redisCacheManager; }
See the remaining creation facets to cache method information Part One
Source address:
https://github.com/wyh-spring-ecosystem-student/spring-boot-student/tree/releases
spring-boot-student-cache-redis-2 project