Redis實(shí)戰(zhàn)

一、緩存驚群

1、場(chǎng)景描述

用戶數(shù)據(jù)寫入和查詢,緩存設(shè)計(jì),寫入的時(shí)候,庫(kù)+緩存,雙寫,緩存默認(rèn) 2 天多隨機(jī)時(shí)間
的過(guò)期,讀的時(shí)候,讀延期,頻繁讀,緩存會(huì)不停的延期,沒(méi)有人查呢,過(guò)期,避免占用緩
存的空間,緩存沒(méi)查到,從數(shù)據(jù)庫(kù)里提出來(lái),放到緩存里去
每次寫入緩存的時(shí)候,為什么一定要設(shè)置 2 天+隨機(jī)幾個(gè)小時(shí)的時(shí)間呢?
答案,緩存驚群的問(wèn)題,緩存一批數(shù)據(jù),突然之間一起都沒(méi)了,過(guò)期時(shí)間設(shè)置的都是一樣的,
緩存集群都驚了,數(shù)據(jù)庫(kù)也驚了,大量緩存同時(shí)過(guò)期->驚群 ->瞬時(shí)大量請(qǐng)求走到 mysql 去
造成壓力
大量的緩存數(shù)據(jù),過(guò)期時(shí)間都是隨機(jī),不要集中在某個(gè)時(shí)間點(diǎn)一起過(guò)期
驚群,技術(shù)里典型的術(shù)語(yǔ),突然在某個(gè)時(shí)間點(diǎn),出了一個(gè)故障,一大片范圍線程/進(jìn)程/機(jī)器,
都同時(shí)被驚動(dòng)了,驚群效應(yīng)

2、解決方案

每次寫入緩存的時(shí)候,設(shè)置 2 天+隨機(jī)幾個(gè)小時(shí)的時(shí)間,使得緩存數(shù)據(jù)不會(huì)一起失效
代碼實(shí)現(xiàn)

 redisCache.set(RedisKeyConstants.USER_INFO_PREFIX + cookbookUserDO.getId(),
                    JsonUtil.object2Json(cookbookUserDTO), CacheSupport.generateCacheExpireSecond());


    /**
     * 生成緩存過(guò)期時(shí)間
     * 2天加上隨機(jī)幾小時(shí)
     *
     * @return
     */
    static Integer generateCacheExpireSecond() {
        return TWO_DAYS_SECONDS + RandomUtil.genRandomInt(0, 10) * 60 * 60;
    }

二、緩存穿透

1、場(chǎng)景描述

緩存穿透的一個(gè)問(wèn)題,穿透->讀取緩存沒(méi)讀到->從 db 里讀->也沒(méi)讀到->bug->高并發(fā)的讀一個(gè)數(shù)據(jù),緩存和 db 都沒(méi)有->每次高并發(fā)讀取,緩存都會(huì)被穿透過(guò)去,每次都要讀一下db,導(dǎo)致高并發(fā)空請(qǐng)求都針對(duì) db 再走,壓力

2、解決方案

先從緩存中獲取數(shù)據(jù),如果緩存中不存在,則從數(shù)據(jù)庫(kù)中查出來(lái),如果數(shù)據(jù)庫(kù)中查出來(lái)的數(shù)據(jù)為空,則賦值為"{}",并且設(shè)置失效時(shí)間。

 @Override
    public CookbookUserDTO getUserInfo(CookbookUserQueryRequest request) {
        Long userId = request.getUserId();

        CookbookUserDTO user = getUserFromCache(userId);
        if(user != null) {
            return user;
        }

        return getUserInfoFromDB(userId);
    }

    private CookbookUserDTO getUserFromCache(Long userId) {
        String userInfoKey = RedisKeyConstants.USER_INFO_PREFIX + userId;
        String userInfoJsonString = redisCache.get(userInfoKey);
        log.info("從緩存中獲取作者信息,userId:{},value:{}", userId, userInfoJsonString);

        if (StringUtils.hasLength(userInfoJsonString)){
            // 防止緩存穿透
            if (Objects.equals(CacheSupport.EMPTY_CACHE, userInfoJsonString)) {
                return new CookbookUserDTO();
            }
            redisCache.expire(RedisKeyConstants.USER_INFO_PREFIX + userId,
                    CacheSupport.generateCacheExpireSecond());
            CookbookUserDTO dto = JsonUtil.json2Object(userInfoJsonString, CookbookUserDTO.class);
            return dto;
        }

        return null;
    }


private CookbookUserDTO getUserInfoFromDB(Long userId) {
        // 有兩個(gè)選擇,load from db + write redis,加兩把鎖,user_lock,user_update_lock
        // 基于redisson,加多鎖,multi lock
        // 共用一把鎖,multi lock加鎖,不同的鎖應(yīng)對(duì)的是不同的并發(fā)場(chǎng)景

//        String userLockKey = RedisKeyConstants.USER_LOCK_PREFIX + userId;

        // 有大量的線程突然讀一個(gè)冷門的用戶數(shù)據(jù),都囤積在這里,在上面大家都沒(méi)讀到
        // 都在這個(gè)地方在排隊(duì)等待獲取鎖,然后去嘗試load db + write redis
        // 非常嚴(yán)重的鎖競(jìng)爭(zhēng)的問(wèn)題,線程,串行化,一個(gè)一個(gè)的排隊(duì),一個(gè)人先拿鎖,load一次db,寫緩存
        // 下一個(gè)人拿到鎖了,通過(guò)double check,直接讀緩存,下一個(gè)人,短時(shí)間內(nèi)突然有一個(gè)嚴(yán)重串行化,雖然每次讀緩存,時(shí)間不多

        // 其實(shí)只要有第一個(gè)人,能夠拿到鎖,進(jìn)去,laod db + wreite redis,redis里就已經(jīng)有數(shù)據(jù)了
        // 后續(xù)的線程就不需要通過(guò)鎖排隊(duì),串行化,一個(gè)一個(gè)load redis里的數(shù)據(jù)
        // 只要有一個(gè)人能夠成功,其他的人,其實(shí)可以突然之間全部轉(zhuǎn)換為上面的操作,無(wú)鎖的情況下,大量的一起并發(fā)的讀redis就可以了

        String userLockKey = RedisKeyConstants.USER_UPDATE_LOCK_PREFIX + userId;
        boolean lock = false;
        try {
            lock = redisLock.tryLock(userLockKey, USER_UPDATE_LOCK_TIMEOUT);
        } catch(InterruptedException e) {
            CookbookUserDTO user = getUserFromCache(userId);
            if(user != null) {
                return user;
            }
            log.error(e.getMessage(), e);
            throw new BaseBizException("查詢失敗");
        }

        if (!lock) {
            CookbookUserDTO user = getUserFromCache(userId);
            if(user != null) {
                return user;
            }
            log.info("緩存數(shù)據(jù)為空,從數(shù)據(jù)庫(kù)查詢作者信息時(shí)獲取鎖失敗,userId:{}", userId);
            throw new BaseBizException("查詢失敗");
        }

        try {
            CookbookUserDTO user = getUserFromCache(userId);
            if(user != null) {
                return user;
            }

            log.info("緩存數(shù)據(jù)為空,從數(shù)據(jù)庫(kù)中獲取數(shù)據(jù),userId:{}", userId);

            String userInfoKey = RedisKeyConstants.USER_INFO_PREFIX + userId;

            // 在這里先讀到了db里的用戶信息的舊數(shù)據(jù)
            // 這個(gè)線程剛剛讀到,還沒(méi)有來(lái)得及把舊數(shù)據(jù)寫入緩存里去
            CookbookUserDO cookbookUserDO = cookbookUserDAO.getById(userId);
            if (Objects.isNull(cookbookUserDO)) {
                redisCache.set(userInfoKey, CacheSupport.EMPTY_CACHE, CacheSupport.generateCachePenetrationExpireSecond());
                return null;
            }

            CookbookUserDTO dto = cookbookUserConverter.convertCookbookUserDTO(cookbookUserDO);

            // 此時(shí)這個(gè)線程,在上面的那個(gè)線程都已經(jīng)把新數(shù)據(jù)寫入緩存里去了,緩存里已經(jīng)是最新數(shù)據(jù)了
            // 把舊數(shù)據(jù)庫(kù),寫入了緩存做了一個(gè)覆蓋操作,典型的,數(shù)據(jù)庫(kù)+緩存雙寫的時(shí)候,寫和讀,并發(fā)的時(shí)候
            // db里是新數(shù)據(jù),緩存里是舊數(shù)據(jù),舊數(shù)據(jù)是覆蓋了新數(shù)據(jù)的
            // db和緩存,數(shù)據(jù)是不一致的
            redisCache.set(userInfoKey, JsonUtil.object2Json(dto), CacheSupport.generateCacheExpireSecond());
            return dto;
        } finally {
            redisLock.unlock(userLockKey);
        }
    }

三、緩存一致性

1、場(chǎng)景描述

如果數(shù)據(jù)庫(kù)中的某條數(shù)據(jù),放入緩存之后,又立馬被更新了,那么該如何更新緩存呢?不更新緩存行不行?
答:當(dāng)然不行,如果不更新緩存,在很長(zhǎng)的一段時(shí)間內(nèi)(決定于緩存的過(guò)期時(shí)間),用戶請(qǐng)求從緩存中獲取到的都可能是舊值,而非數(shù)據(jù)庫(kù)的最新值。這不是有數(shù)據(jù)不一致的問(wèn)題?

2、解決方案
image.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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