Spring Boot緩存實戰(zhàn) Redis 設置有效時間和自動刷新緩存-2

問題

上一篇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() + "數(shù)據(jù)做了緩存");
    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調(diào)用getCache(name)獲取緩存的時候,當沒有找到緩存的時候會調(diào)用getMissingCache(String cacheName)來新建緩存。在新建緩存的時候我們可以在擴展的Map<String, CacheTime> cacheTimes里面根據(jù)key獲取CacheTime進而拿到有效時間和自動刷新時間。

具體實現(xiàn)

我們先新建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類一樣,主要解決:

  • 獲取緩存的在大并發(fā)下的一個bug,詳情
  • 在獲取緩存的時候判斷一下緩存的過期時間和自動刷新時間,根據(jù)這個值去刷新緩存

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的性能,啟用后臺線程去完成緩存的刷。
     * 并且只放一個線程去刷新數(shù)據(jù)。
     *
     * @param key
     * @return
     */
    @Override
    public ValueWrapper get(final Object key) {
        RedisCacheKey cacheKey = getRedisCacheKey(key);
        String cacheKeyStr = getCacheKey(key);
        // 調(diào)用重寫后的get方法
        ValueWrapper valueWrapper = this.get(cacheKey);

        if (null != valueWrapper) {
            // 刷新緩存數(shù)據(jù)
            refreshCache(key, cacheKeyStr);
        }
        return valueWrapper;
    }

    /**
     * 重寫父類的get函數(shù)。
     * 父類的get方法,是先使用exists判斷key是否存在,不存在返回null,存在再到redis緩存中去取值。這樣會導致并發(fā)問題,
     * 假如有一個請求調(diào)用了exists函數(shù)判斷key存在,但是在下一時刻這個緩存過期了,或者被刪掉了。
     * 這時候再去緩存中獲取值的時候返回的就是null了。
     * 可以先獲取緩存的值,再去判斷key是否存在。
     *
     * @param cacheKey
     * @return
     */
    @Override
    public RedisCacheElement get(final RedisCacheKey cacheKey) {

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

        // 根據(jù)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;
    }

    /**
     * 刷新緩存數(shù)據(jù)
     */
    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()) {
                            // 獲取鎖之后再判斷一下過期時間,看是否需要加載數(shù)據(jù)
                            Long ttl = CustomizedRedisCache.this.redisOperations.getExpire(cacheKeyStr);
                            if (null != ttl && ttl <= CustomizedRedisCache.this.preloadSecondTime) {
                                // 通過獲取代理方法信息重新加載緩存數(shù)據(jù)
                                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)方法獲取緩存的時候,當沒有找到緩存回去調(diào)用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;
    }

    /**
     * 創(chuàng)建緩存
     *
     * @param cacheName 緩存名稱
     * @return
     */
    public CustomizedRedisCache getMissingCache(String cacheName) {

        // 有效時間,初始化獲取默認的有效時間
        Long expirationSecondTime = getExpirationSecondTime(cacheName);
        // 自動刷新時間,默認是0
        Long preloadSecondTime = getPreloadSecondTime(cacheName);

        logger.info("緩存 cacheName:{},過期時間:{}, 自動刷新時間:{}", cacheName, expirationSecondTime, preloadSecondTime);
        // 是否在運行時創(chuàng)建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;
    }

    /**
     * 根據(jù)緩存名稱設置緩存的有效時間和刷新時間,單位秒
     *
     * @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;
}

剩余的創(chuàng)建切面來緩存方法信息請看上篇

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

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

為監(jiān)控而生的多級緩存框架 layering-cache這是我開源的一個多級緩存框架的實現(xiàn),如果有興趣可以看一下

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容