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

問(wèn)題

上一篇Spring Boot Cache + redis 設(shè)置有效時(shí)間和自動(dòng)刷新緩存,時(shí)間支持在配置文件中配置晨雳,說(shuō)了一種時(shí)間方式锋谐,直接擴(kuò)展注解的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;
}

但是這種方式有一個(gè)弊端就是破壞了原有Spring Cache架構(gòu),導(dǎo)致如果后期想換緩存就會(huì)去改很多代碼翩肌。

解決思路

RedisCacheManager可以在配置CacheManager的Bean的時(shí)候指定過(guò)期時(shí)間模暗,如:

@Bean
public RedisCacheManager cacheManager(RedisTemplate<String, Object> redisTemplate) {
    RedisCacheManager redisCacheManager = new RedisCacheManager(redisTemplate);
    // 開(kāi)啟使用緩存名稱(chēng)最為key前綴
    redisCacheManager.setUsePrefix(true);
    //這里可以設(shè)置一個(gè)默認(rèn)的過(guò)期時(shí)間 單位是秒
    redisCacheManager.setDefaultExpiration(redisDefaultExpiration);

    // 設(shè)置緩存的過(guò)期時(shí)間
    Map<String, Long> expires = new HashMap<>();
    expires.put("people", 1000);
    redisCacheManager.setExpires(expires);

    return redisCacheManager;
}

我們借鑒一下redisCacheManager.setExpires(expires)思路,進(jìn)行擴(kuò)展念祭。直接新建一個(gè)CacheTime類(lèi)兑宇,來(lái)存過(guò)期時(shí)間和自動(dòng)刷新時(shí)間。

在RedisCacheManager調(diào)用getCache(name)獲取緩存的時(shí)候粱坤,當(dāng)沒(méi)有找到緩存的時(shí)候會(huì)調(diào)用getMissingCache(String cacheName)來(lái)新建緩存隶糕。在新建緩存的時(shí)候我們可以在擴(kuò)展的Map<String, CacheTime> cacheTimes里面根據(jù)key獲取CacheTime進(jìn)而拿到有效時(shí)間和自動(dòng)刷新時(shí)間瓷产。

具體實(shí)現(xiàn)

我們先新建CacheTime類(lèi)

CacheTime:

 /**
 * @author yuhao.wang
 */
public class CacheTime {
    public CacheTime(long preloadSecondTime, long expirationSecondTime) {
        this.preloadSecondTime = preloadSecondTime;
        this.expirationSecondTime = expirationSecondTime;
    }

    /**
     * 緩存主動(dòng)在失效前強(qiáng)制刷新緩存的時(shí)間
     * 單位:秒
     */
    private long preloadSecondTime = 0;

    /**
     * 緩存有效時(shí)間
     */
    private long expirationSecondTime;

    public long getPreloadSecondTime() {
        return preloadSecondTime;
    }

    public long getExpirationSecondTime() {
        return expirationSecondTime;
    }
}

擴(kuò)展一下RedisCache類(lèi)

和上一篇的CustomizedRedisCache類(lèi)一樣,主要解決:

  • 獲取緩存的在大并發(fā)下的一個(gè)bug枚驻,詳情
  • 在獲取緩存的時(shí)候判斷一下緩存的過(guò)期時(shí)間和自動(dòng)刷新時(shí)間濒旦,根據(jù)這個(gè)值去刷新緩存

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;

    /**
     * 緩存主動(dòng)在失效前強(qiáng)制刷新緩存的時(shí)間
     * 單位:秒
     */
    private long preloadSecondTime = 0;

    /**
     * 緩存有效時(shí)間
     */
    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;
        // 指定有效時(shí)間
        this.expirationSecondTime = expiration;
        // 指定自動(dòng)刷新時(shí)間
        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;
        // 指定有效時(shí)間
        this.expirationSecondTime = expiration;
        // 指定自動(dòng)刷新時(shí)間
        this.preloadSecondTime = preloadSecondTime;
        this.prefix = prefix;

    }

    /**
     * 重寫(xiě)get方法,獲取到緩存后再次取緩存剩余的時(shí)間再登,如果時(shí)間小余我們配置的刷新時(shí)間就手動(dòng)刷新緩存尔邓。
     * 為了不影響get的性能,啟用后臺(tái)線(xiàn)程去完成緩存的刷锉矢。
     * 并且只放一個(gè)線(xiàn)程去刷新數(shù)據(jù)铃拇。
     *
     * @param key
     * @return
     */
    @Override
    public ValueWrapper get(final Object key) {
        RedisCacheKey cacheKey = getRedisCacheKey(key);
        String cacheKeyStr = getCacheKey(key);
        // 調(diào)用重寫(xiě)后的get方法
        ValueWrapper valueWrapper = this.get(cacheKey);

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

    /**
     * 重寫(xiě)父類(lèi)的get函數(shù)。
     * 父類(lèi)的get方法沈撞,是先使用exists判斷key是否存在,不存在返回null雕什,存在再到redis緩存中去取值缠俺。這樣會(huì)導(dǎo)致并發(fā)問(wèn)題,
     * 假如有一個(gè)請(qǐng)求調(diào)用了exists函數(shù)判斷key存在贷岸,但是在下一時(shí)刻這個(gè)緩存過(guò)期了壹士,或者被刪掉了。
     * 這時(shí)候再去緩存中獲取值的時(shí)候返回的就是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) {
            // 盡量少的去開(kāi)啟線(xiàn)程螟蒸,因?yàn)榫€(xiàn)程池是有限的
            ThreadTaskUtils.run(new Runnable() {
                @Override
                public void run() {
                    // 加一個(gè)分布式鎖盒使,只放一個(gè)請(qǐng)求去刷新緩存
                    RedisLock redisLock = new RedisLock((RedisTemplate) redisOperations, cacheKeyStr + "_lock");
                    try {
                        if (redisLock.lock()) {
                            // 獲取鎖之后再判斷一下過(guò)期時(shí)間,看是否需要加載數(shù)據(jù)
                            Long ttl = CustomizedRedisCache.this.redisOperations.getExpire(cacheKeyStr);
                            if (null != ttl && ttl <= CustomizedRedisCache.this.preloadSecondTime) {
                                // 通過(guò)獲取代理方法信息重新加載緩存數(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());
    }
}

擴(kuò)展RedisCacheManager

主要擴(kuò)展通過(guò)getCache(String name)方法獲取緩存的時(shí)候七嫌,當(dāng)沒(méi)有找到緩存回去調(diào)用getMissingCache(String cacheName)來(lái)新建緩存少办。

CustomizedRedisCacheManager:

/**
 * 自定義的redis緩存管理器
 * 支持方法上配置過(guò)期時(shí)間
 * 支持熱加載緩存:緩存即將過(guò)期時(shí)主動(dòng)刷新緩存
 *
 * @author yuhao.wang
 */
public class CustomizedRedisCacheManager extends RedisCacheManager {

    private static final Logger logger = LoggerFactory.getLogger(CustomizedRedisCacheManager.class);

    /**
     * 父類(lèi)dynamic字段
     */
    private static final String SUPER_FIELD_DYNAMIC = "dynamic";

    /**
     * 父類(lèi)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;
    }

    /**
     * 獲取過(guò)期時(shí)間
     *
     * @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;
    }

    /**
     * 獲取自動(dòng)刷新時(shí)間
     *
     * @return
     */
    private long getPreloadSecondTime(String name) {
        // 自動(dòng)刷新時(shí)間,默認(rèn)是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 緩存名稱(chēng)
     * @return
     */
    public CustomizedRedisCache getMissingCache(String cacheName) {

        // 有效時(shí)間诵原,初始化獲取默認(rèn)的有效時(shí)間
        Long expirationSecondTime = getExpirationSecondTime(cacheName);
        // 自動(dòng)刷新時(shí)間英妓,默認(rèn)是0
        Long preloadSecondTime = getPreloadSecondTime(cacheName);

        logger.info("緩存 cacheName:{},過(guò)期時(shí)間:{}, 自動(dòng)刷新時(shí)間:{}", cacheName, expirationSecondTime, preloadSecondTime);
        // 是否在運(yùn)行時(shí)創(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ù)緩存名稱(chēng)設(shè)置緩存的有效時(shí)間和刷新時(shí)間绍赛,單位秒
     *
     * @param cacheTimes
     */
    public void setCacheTimess(Map<String, CacheTime> cacheTimes) {
        this.cacheTimes = (cacheTimes != null ? new ConcurrentHashMap<String, CacheTime>(cacheTimes) : null);
    }

    /**
     * 設(shè)置默認(rèn)的過(guò)去時(shí)間蔓纠, 單位:秒
     *
     * @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);
    // 開(kāi)啟使用緩存名稱(chēng)最為key前綴
    redisCacheManager.setUsePrefix(true);
    //這里可以設(shè)置一個(gè)默認(rèn)的過(guò)期時(shí)間 單位是秒
    redisCacheManager.setDefaultExpiration(redisDefaultExpiration);

    // 設(shè)置緩存的過(guò)期時(shí)間和自動(dòng)刷新時(shí)間
    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)建切面來(lái)緩存方法信息請(qǐng)看上篇

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

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末吗蚌,一起剝皮案震驚了整個(gè)濱河市腿倚,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌褪测,老刑警劉巖猴誊,帶你破解...
    沈念sama閱讀 218,546評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件潦刃,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡懈叹,警方通過(guò)查閱死者的電腦和手機(jī)乖杠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)澄成,“玉大人胧洒,你說(shuō)我怎么就攤上這事∧矗” “怎么了卫漫?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,911評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀(guān)的道長(zhǎng)肾砂。 經(jīng)常有香客問(wèn)我列赎,道長(zhǎng),這世上最難降的妖魔是什么镐确? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,737評(píng)論 1 294
  • 正文 為了忘掉前任包吝,我火速辦了婚禮,結(jié)果婚禮上源葫,老公的妹妹穿的比我還像新娘诗越。我一直安慰自己,他們只是感情好息堂,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,753評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布嚷狞。 她就那樣靜靜地躺著,像睡著了一般荣堰。 火紅的嫁衣襯著肌膚如雪床未。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,598評(píng)論 1 305
  • 那天持隧,我揣著相機(jī)與錄音即硼,去河邊找鬼。 笑死屡拨,一個(gè)胖子當(dāng)著我的面吹牛只酥,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播呀狼,決...
    沈念sama閱讀 40,338評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼裂允,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了哥艇?” 一聲冷哼從身側(cè)響起绝编,我...
    開(kāi)封第一講書(shū)人閱讀 39,249評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后十饥,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體窟勃,經(jīng)...
    沈念sama閱讀 45,696評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,888評(píng)論 3 336
  • 正文 我和宋清朗相戀三年逗堵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了秉氧。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,013評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蜒秤,死狀恐怖汁咏,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情作媚,我是刑警寧澤攘滩,帶...
    沈念sama閱讀 35,731評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站纸泡,受9級(jí)特大地震影響漂问,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜女揭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,348評(píng)論 3 330
  • 文/蒙蒙 一级解、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧田绑,春花似錦、人聲如沸抡爹。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,929評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)冬竟。三九已至欧穴,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間泵殴,已是汗流浹背涮帘。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,048評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留笑诅,地道東北人调缨。 一個(gè)月前我還...
    沈念sama閱讀 48,203評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像吆你,于是被迫代替她去往敵國(guó)和親弦叶。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,960評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容