Spring Boot Cache + redis Sets Valid Time and Automatic Refresh Cache-2

Keywords: Spring Java Redis github

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

There are red envelopes in the sweep yard.

Posted by gwh on Tue, 08 Jan 2019 19:18:09 -0800