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

问题

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

@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架构,致使若是后期想换缓存就会去改不少代码。git

解决思路

RedisCacheManager能够在配置CacheManager的Bean的时候指定过时时间,如:github

@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类,来存过时时间和自动刷新时间。redis

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

具体实现

咱们先新建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)来新建缓存。app

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 工程

为监控而生的多级缓存框架 layering-cache这是我开源的一个多级缓存框架的实现,若是有兴趣能够看一下

相关文章
相关标签/搜索