美文网首页REDISjava程序员
Spring Boot缓存实战 Redis 设置有效时间和自动刷

Spring Boot缓存实战 Redis 设置有效时间和自动刷

作者: xiaolyuh | 来源:发表于2017-12-16 14:47 被阅读4701次

    问题

    上一篇Spring Boot Cache + redis 设置有效时间和自动刷新缓存,时间支持在配置文件中配置,说了一种时间方式,直接扩展注解的Value值,如:

    @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("为id、key为:" + p.getId() + "数据做了缓存");
        System.out.println(redisTemplate);
        return p;
    }
    

    但是这种方式有一个弊端就是破坏了原有Spring Cache架构,导致如果后期想换缓存就会去改很多代码。

    解决思路

    RedisCacheManager可以在配置CacheManager的Bean的时候指定过期时间,如:

    @Bean
    public RedisCacheManager cacheManager(RedisTemplate<String, Object> redisTemplate) {
        RedisCacheManager redisCacheManager = new RedisCacheManager(redisTemplate);
        // 开启使用缓存名称最为key前缀
        redisCacheManager.setUsePrefix(true);
        //这里可以设置一个默认的过期时间 单位是秒
        redisCacheManager.setDefaultExpiration(redisDefaultExpiration);
    
        // 设置缓存的过期时间
        Map<String, Long> expires = new HashMap<>();
        expires.put("people", 1000);
        redisCacheManager.setExpires(expires);
    
        return redisCacheManager;
    }
    

    我们借鉴一下redisCacheManager.setExpires(expires)思路,进行扩展。直接新建一个CacheTime类,来存过期时间和自动刷新时间。

    在RedisCacheManager调用getCache(name)获取缓存的时候,当没有找到缓存的时候会调用getMissingCache(String cacheName)来新建缓存。在新建缓存的时候我们可以在扩展的Map<String, CacheTime> cacheTimes里面根据key获取CacheTime进而拿到有效时间和自动刷新时间。

    具体实现

    我们先新建CacheTime类

    CacheTime:

     /**
     * @author yuhao.wang
     */
    public class CacheTime {
        public CacheTime(long preloadSecondTime, long expirationSecondTime) {
            this.preloadSecondTime = preloadSecondTime;
            this.expirationSecondTime = expirationSecondTime;
        }
    
        /**
         * 缓存主动在失效前强制刷新缓存的时间
         * 单位:秒
         */
        private long preloadSecondTime = 0;
    
        /**
         * 缓存有效时间
         */
        private long expirationSecondTime;
    
        public long getPreloadSecondTime() {
            return preloadSecondTime;
        }
    
        public long getExpirationSecondTime() {
            return expirationSecondTime;
        }
    }
    

    扩展一下RedisCache类

    和上一篇的CustomizedRedisCache类一样,主要解决:

    • 获取缓存的在大并发下的一个bug,详情
    • 在获取缓存的时候判断一下缓存的过期时间和自动刷新时间,根据这个值去刷新缓存

    CustomizedRedisCache:

    /**
     * 自定义的redis缓存
     *
     * @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;
    
        /**
         * 缓存主动在失效前强制刷新缓存的时间
         * 单位:秒
         */
        private long preloadSecondTime = 0;
    
        /**
         * 缓存有效时间
         */
        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;
            // 指定有效时间
            this.expirationSecondTime = expiration;
            // 指定自动刷新时间
            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;
            // 指定有效时间
            this.expirationSecondTime = expiration;
            // 指定自动刷新时间
            this.preloadSecondTime = preloadSecondTime;
            this.prefix = prefix;
    
        }
    
        /**
         * 重写get方法,获取到缓存后再次取缓存剩余的时间,如果时间小余我们配置的刷新时间就手动刷新缓存。
         * 为了不影响get的性能,启用后台线程去完成缓存的刷。
         * 并且只放一个线程去刷新数据。
         *
         * @param key
         * @return
         */
        @Override
        public ValueWrapper get(final Object key) {
            RedisCacheKey cacheKey = getRedisCacheKey(key);
            String cacheKeyStr = getCacheKey(key);
            // 调用重写后的get方法
            ValueWrapper valueWrapper = this.get(cacheKey);
    
            if (null != valueWrapper) {
                // 刷新缓存数据
                refreshCache(key, cacheKeyStr);
            }
            return valueWrapper;
        }
    
        /**
         * 重写父类的get函数。
         * 父类的get方法,是先使用exists判断key是否存在,不存在返回null,存在再到redis缓存中去取值。这样会导致并发问题,
         * 假如有一个请求调用了exists函数判断key存在,但是在下一时刻这个缓存过期了,或者被删掉了。
         * 这时候再去缓存中获取值的时候返回的就是null了。
         * 可以先获取缓存的值,再去判断key是否存在。
         *
         * @param cacheKey
         * @return
         */
        @Override
        public RedisCacheElement get(final RedisCacheKey cacheKey) {
    
            Assert.notNull(cacheKey, "CacheKey must not be null!");
    
            // 根据key获取缓存值
            RedisCacheElement redisCacheElement = new RedisCacheElement(cacheKey, fromStoreValue(lookup(cacheKey)));
            // 判断key是否存在
            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;
        }
    
        /**
         * 刷新缓存数据
         */
        private void refreshCache(Object key, String cacheKeyStr) {
            Long ttl = this.redisOperations.getExpire(cacheKeyStr);
            if (null != ttl && ttl <= CustomizedRedisCache.this.preloadSecondTime) {
                // 尽量少的去开启线程,因为线程池是有限的
                ThreadTaskUtils.run(new Runnable() {
                    @Override
                    public void run() {
                        // 加一个分布式锁,只放一个请求去刷新缓存
                        RedisLock redisLock = new RedisLock((RedisTemplate) redisOperations, cacheKeyStr + "_lock");
                        try {
                            if (redisLock.lock()) {
                                // 获取锁之后再判断一下过期时间,看是否需要加载数据
                                Long ttl = CustomizedRedisCache.this.redisOperations.getExpire(cacheKeyStr);
                                if (null != ttl && ttl <= CustomizedRedisCache.this.preloadSecondTime) {
                                    // 通过获取代理方法信息重新加载缓存数据
                                    CustomizedRedisCache.this.getCacheSupport().refreshCacheByKey(CustomizedRedisCache.super.getName(), cacheKeyStr);
                                }
                            }
                        } catch (Exception e) {
                            logger.info(e.getMessage(), e);
                        } finally {
                            redisLock.unlock();
                        }
                    }
                });
            }
        }
    
        public long getExpirationSecondTime() {
            return expirationSecondTime;
        }
    
    
        /**
         * 获取RedisCacheKey
         *
         * @param key
         * @return
         */
        public RedisCacheKey getRedisCacheKey(Object key) {
    
            return new RedisCacheKey(key).usePrefix(this.prefix)
                    .withKeySerializer(redisOperations.getKeySerializer());
        }
    
        /**
         * 获取RedisCacheKey
         *
         * @param key
         * @return
         */
        public String getCacheKey(Object key) {
            return new String(getRedisCacheKey(key).getKeyBytes());
        }
    }
    

    扩展RedisCacheManager

    主要扩展通过getCache(String name)方法获取缓存的时候,当没有找到缓存回去调用getMissingCache(String cacheName)来新建缓存。

    CustomizedRedisCacheManager:

    /**
     * 自定义的redis缓存管理器
     * 支持方法上配置过期时间
     * 支持热加载缓存:缓存即将过期时主动刷新缓存
     *
     * @author yuhao.wang
     */
    public class CustomizedRedisCacheManager extends RedisCacheManager {
    
        private static final Logger logger = LoggerFactory.getLogger(CustomizedRedisCacheManager.class);
    
        /**
         * 父类dynamic字段
         */
        private static final String SUPER_FIELD_DYNAMIC = "dynamic";
    
        /**
         * 父类cacheNullValues字段
         */
        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;
        }
    
        /**
         * 获取过期时间
         *
         * @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;
        }
    
        /**
         * 获取自动刷新时间
         *
         * @return
         */
        private long getPreloadSecondTime(String name) {
            // 自动刷新时间,默认是0
            CacheTime cacheTime = null;
            if (!CollectionUtils.isEmpty(cacheTimes)) {
                cacheTime = cacheTimes.get(name);
            }
            Long preloadSecondTime = cacheTime != null ? cacheTime.getPreloadSecondTime() : 0;
            return preloadSecondTime < 0 ? 0 : preloadSecondTime;
        }
    
        /**
         * 创建缓存
         *
         * @param cacheName 缓存名称
         * @return
         */
        public CustomizedRedisCache getMissingCache(String cacheName) {
    
            // 有效时间,初始化获取默认的有效时间
            Long expirationSecondTime = getExpirationSecondTime(cacheName);
            // 自动刷新时间,默认是0
            Long preloadSecondTime = getPreloadSecondTime(cacheName);
    
            logger.info("缓存 cacheName:{},过期时间:{}, 自动刷新时间:{}", cacheName, expirationSecondTime, preloadSecondTime);
            // 是否在运行时创建Cache
            Boolean dynamic = (Boolean) ReflectionUtils.getFieldValue(getInstance(), SUPER_FIELD_DYNAMIC);
            // 是否允许存放NULL
            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;
        }
    
        /**
         * 根据缓存名称设置缓存的有效时间和刷新时间,单位秒
         *
         * @param cacheTimes
         */
        public void setCacheTimess(Map<String, CacheTime> cacheTimes) {
            this.cacheTimes = (cacheTimes != null ? new ConcurrentHashMap<String, CacheTime>(cacheTimes) : null);
        }
    
        /**
         * 设置默认的过去时间, 单位:秒
         *
         * @param defaultExpireTime
         */
        @Override
        public void setDefaultExpiration(long defaultExpireTime) {
            super.setDefaultExpiration(defaultExpireTime);
            this.defaultExpiration = defaultExpireTime;
        }
    
        @Deprecated
        @Override
        public void setExpires(Map<String, Long> expires) {
    
        }
    }
    

    在Config中配置RedisCacheManager

    @Bean
    public RedisCacheManager cacheManager(RedisTemplate<String, Object> redisTemplate) {
        CustomizedRedisCacheManager redisCacheManager = new CustomizedRedisCacheManager(redisTemplate);
        // 开启使用缓存名称最为key前缀
        redisCacheManager.setUsePrefix(true);
        //这里可以设置一个默认的过期时间 单位是秒
        redisCacheManager.setDefaultExpiration(redisDefaultExpiration);
    
        // 设置缓存的过期时间和自动刷新时间
        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;
    }
    

    剩余的创建切面来缓存方法信息请看上篇

    源码地址:
    https://github.com/wyh-spring-ecosystem-student/spring-boot-student/tree/releases

    spring-boot-student-cache-redis-2 工程

    扫码有红包哦

    相关文章

      网友评论

        本文标题:Spring Boot缓存实战 Redis 设置有效时间和自动刷

        本文链接:https://www.haomeiwen.com/subject/enuswxtx.html