Spring Redis Cache @Cacheable 大并發(fā)下返回null

問題描述

最近我們用Spring Cache + redis來做緩存蛙卤。在高并發(fā)下@Cacheable 注解返回的內(nèi)容是null粘姜。查看了一下源代碼,在使用注解獲取緩存的時(shí)候全跨,RedisCache的get方法會(huì)先去判斷key是否存在,然后再去獲取值亿遂。這了就有一個(gè)漏銅浓若,當(dāng)線程1判斷了key是存在的,緊接著這個(gè)時(shí)候這個(gè)key過期了蛇数,這時(shí)線程1再去獲取值的時(shí)候返回的是null挪钓。

RedisCache的get方法源碼:

public RedisCacheElement get(final RedisCacheKey cacheKey) {

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

    // 判斷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;
    }
    
    // 獲取key對(duì)應(yīng)的值
    return new RedisCacheElement(cacheKey, fromStoreValue(lookup(cacheKey)));
}

// 獲取值
protected Object lookup(Object key) {

    RedisCacheKey cacheKey = key instanceof RedisCacheKey ? (RedisCacheKey) key : getRedisCacheKey(key);

    byte[] bytes = (byte[]) redisOperations.execute(new AbstractRedisCacheCallback<byte[]>(
            new BinaryRedisCacheElement(new RedisCacheElement(cacheKey, null), cacheValueAccessor), cacheMetadata) {

        @Override
        public byte[] doInRedis(BinaryRedisCacheElement element, RedisConnection connection) throws DataAccessException {
            return connection.get(element.getKeyBytes());
        }
    });

    return bytes == null ? null : cacheValueAccessor.deserializeIfNecessary(bytes);
}

解決方案

這個(gè)流程有問題,解決方案就是把這個(gè)流程倒過來耳舅,先去獲取值碌上,然后去判斷這個(gè)key是否存在。不能直接用獲取的值根據(jù)是否是NULL判斷是否有值浦徊,因?yàn)镽eids可能緩存NULL值馏予。

重寫RedisCache的get方法:

public RedisCacheElement get(final RedisCacheKey cacheKey) {

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

    RedisCacheElement redisCacheElement = new RedisCacheElement(cacheKey, fromStoreValue(lookup(cacheKey)));
    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í)現(xiàn):

重寫RedisCache的get方法

package com.xiaolyuh.redis.cache;

import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheElement;
import org.springframework.data.redis.cache.RedisCacheKey;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.util.Assert;

/**
 * 自定義的redis緩存
 *
 * @author yuhao.wang
 */
public class CustomizedRedisCache extends RedisCache {

    private final RedisOperations redisOperations;

    private final byte[] prefix;

    public CustomizedRedisCache(String name, byte[] prefix, RedisOperations<? extends Object, ? extends Object> redisOperations, long expiration) {
        super(name, prefix, redisOperations, expiration);
        this.redisOperations = redisOperations;
        this.prefix = prefix;
    }

    public CustomizedRedisCache(String name, byte[] prefix, RedisOperations<? extends Object, ? extends Object> redisOperations, long expiration, boolean allowNullValues) {
        super(name, prefix, redisOperations, expiration, allowNullValues);
        this.redisOperations = redisOperations;
        this.prefix = prefix;
    }

    /**
     * 重寫父類的get函數(shù)。
     * 父類的get方法盔性,是先使用exists判斷key是否存在吗蚌,不存在返回null,存在再到redis緩存中去取值纯出。這樣會(huì)導(dǎo)致并發(fā)問題,
     * 假如有一個(gè)請(qǐng)求調(diào)用了exists函數(shù)判斷key存在敷燎,但是在下一時(shí)刻這個(gè)緩存過期了暂筝,或者被刪掉了。
     * 這時(shí)候再去緩存中獲取值的時(shí)候返回的就是null了硬贯。
     * 可以先獲取緩存的值焕襟,再去判斷key是否存在。
     *
     * @param cacheKey
     * @return
     */
    @Override
    public RedisCacheElement get(final RedisCacheKey cacheKey) {

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

        RedisCacheElement redisCacheElement = new RedisCacheElement(cacheKey, fromStoreValue(lookup(cacheKey)));
        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;
    }


    /**
     * 獲取RedisCacheKey
     *
     * @param key
     * @return
     */
    private RedisCacheKey getRedisCacheKey(Object key) {
        return new RedisCacheKey(key).usePrefix(this.prefix)
                .withKeySerializer(redisOperations.getKeySerializer());
    }
}

重寫RedisCacheManager

package com.xiaolyuh.redis.cache;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.Cache;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.Collection;

/**
 * 自定義的redis緩存管理器
 * @author yuhao.wang 
 */
public class CustomizedRedisCacheManager extends RedisCacheManager {

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

    public CustomizedRedisCacheManager(RedisOperations redisOperations) {
        super(redisOperations);
    }

    public CustomizedRedisCacheManager(RedisOperations redisOperations, Collection<String> cacheNames) {
        super(redisOperations, cacheNames);
    }

    @Override
    protected Cache getMissingCache(String name) {
        long expiration = computeExpiration(name);
        return new CustomizedRedisCache(
                name,
                (this.isUsePrefix() ? this.getCachePrefix().prefix(name) : null),
                this.getRedisOperations(),
                expiration);
    }
}

配置Redis管理器

@Configuration
public class RedisConfig {

    // redis緩存的有效時(shí)間單位是秒
    @Value("${redis.default.expiration:3600}")
    private long redisDefaultExpiration;

    @Bean
    public RedisCacheManager cacheManager(RedisTemplate<Object, Object> redisTemplate) {
        RedisCacheManager redisCacheManager = new CustomizedRedisCacheManager(redisTemplate);
        redisCacheManager.setUsePrefix(true);
        //這里可以設(shè)置一個(gè)默認(rèn)的過期時(shí)間 單位是秒
        redisCacheManager.setDefaultExpiration(redisDefaultExpiration);

        return redisCacheManager;
    }

    /**
     * 顯示聲明緩存key生成器
     *
     * @return
     */
    @Bean
    public KeyGenerator keyGenerator() {

        return new SimpleKeyGenerator();
    }

}

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

spring-boot-student-cache-redis 工程

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末鸵赖,一起剝皮案震驚了整個(gè)濱河市务漩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌它褪,老刑警劉巖饵骨,帶你破解...
    沈念sama閱讀 212,718評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異茫打,居然都是意外死亡居触,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門老赤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來轮洋,“玉大人,你說我怎么就攤上這事抬旺”子瑁” “怎么了?”我有些...
    開封第一講書人閱讀 158,207評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵开财,是天一觀的道長(zhǎng)汉柒。 經(jīng)常有香客問我,道長(zhǎng)床未,這世上最難降的妖魔是什么竭翠? 我笑而不...
    開封第一講書人閱讀 56,755評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮薇搁,結(jié)果婚禮上斋扰,老公的妹妹穿的比我還像新娘。我一直安慰自己啃洋,他們只是感情好传货,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,862評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著宏娄,像睡著了一般问裕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上孵坚,一...
    開封第一講書人閱讀 50,050評(píng)論 1 291
  • 那天粮宛,我揣著相機(jī)與錄音,去河邊找鬼卖宠。 笑死巍杈,一個(gè)胖子當(dāng)著我的面吹牛扛伍,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播鳖宾,決...
    沈念sama閱讀 39,136評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼渔肩!你這毒婦竟也來了漂问?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,882評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤栏饮,失蹤者是張志新(化名)和其女友劉穎袍嬉,沒想到半個(gè)月后灶平,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,330評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡罐监,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,651評(píng)論 2 327
  • 正文 我和宋清朗相戀三年弓柱,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了侧但。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,789評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡屁药,死狀恐怖酿箭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情七问,我是刑警寧澤茫舶,帶...
    沈念sama閱讀 34,477評(píng)論 4 333
  • 正文 年R本政府宣布饶氏,位于F島的核電站,受9級(jí)特大地震影響疹启,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜挣磨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,135評(píng)論 3 317
  • 文/蒙蒙 一茁裙、第九天 我趴在偏房一處隱蔽的房頂上張望节仿。 院中可真熱鬧,春花似錦廊宪、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)赏僧。三九已至,卻和暖如春挽绩,著一層夾襖步出監(jiān)牢的瞬間唉堪,已是汗流浹背唠亚。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評(píng)論 1 267
  • 我被黑心中介騙來泰國(guó)打工灶搜, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人前酿。 一個(gè)月前我還...
    沈念sama閱讀 46,598評(píng)論 2 362
  • 正文 我出身青樓鹏溯,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親肺孵。 傳聞我的和親對(duì)象是個(gè)殘疾皇子颜阐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,697評(píng)論 2 351

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

  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,778評(píng)論 6 342
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理初婆,服務(wù)發(fā)現(xiàn)猿棉,斷路器,智...
    卡卡羅2017閱讀 134,638評(píng)論 18 139
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語(yǔ)法弊琴,類相關(guān)的語(yǔ)法杖爽,內(nèi)部類的語(yǔ)法,繼承相關(guān)的語(yǔ)法腋寨,異常的語(yǔ)法化焕,線程的語(yǔ)...
    子非魚_t_閱讀 31,602評(píng)論 18 399
  • (衍生文請(qǐng)結(jié)合洞喵文第五章下) - 盛夏是一個(gè)適合熱戀的季節(jié),鬢角青絲與白皙皮膚滲出的細(xì)密汗珠貼合在一起查刻,顧海藍(lán)看...
    蜂蜜柚子與茶閱讀 5,080評(píng)論 1 8
  • 穿越死亡 我在一條靜謐的小路上前行凤类。沒有風(fēng),樹木不動(dòng)佃延。我的速度不快现诀,因?yàn)椴恢肋@路伸向何處履肃。我也沒有目的地。 遠(yuǎn)處...
    冉之閱讀 506評(píng)論 3 0