欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

springboot項(xiàng)目redis緩存異常實(shí)戰(zhàn)案例詳解(提供解決方案)

 更新時(shí)間:2025年05月22日 12:06:18   作者:WalkerShen  
redis基本上是高并發(fā)場(chǎng)景上會(huì)用到的一個(gè)高性能的key-value數(shù)據(jù)庫,屬于nosql類型,一般用作于緩存,一般是結(jié)合數(shù)據(jù)庫一塊使用的,但是在使用的過程中可能會(huì)出現(xiàn)異常的問題,這篇文章主要介紹了springboot項(xiàng)目redis緩存異常實(shí)戰(zhàn)案例詳解(提供解決方案),需要的朋友可以參考下

緩存異常實(shí)踐案例

redis基本上是高并發(fā)場(chǎng)景上會(huì)用到的一個(gè)高性能的key-value數(shù)據(jù)庫,屬于nosql類型,一般用作于緩存,一般是結(jié)合數(shù)據(jù)庫一塊使用的,但是在使用的過程中可能會(huì)出現(xiàn)異常的問題,就是面試常常嘮嗑的緩存異常問題
分別是緩存擊穿,緩存穿透和雪崩,簡單解釋如下:

緩存穿透:
就是當(dāng)用戶查詢數(shù)據(jù)時(shí),緩存和數(shù)據(jù)庫該數(shù)據(jù)都是不存在的,此時(shí)如果用戶不斷的請(qǐng)求,就會(huì)不斷的查詢緩存和數(shù)據(jù)庫,對(duì)數(shù)據(jù)庫造成很大壓力

緩存擊穿:
當(dāng)熱點(diǎn)key過期或者丟失時(shí),大量的請(qǐng)求訪問該數(shù)據(jù),緩存不存在,就會(huì)將大量的請(qǐng)求直接訪問數(shù)據(jù)庫,造成數(shù)據(jù)庫有大量連接,存在崩潰風(fēng)險(xiǎn)

緩存雪崩:
是指大量請(qǐng)求在緩存中沒有查到數(shù)據(jù),直接訪問數(shù)據(jù)庫,導(dǎo)致數(shù)據(jù)庫壓力增大,最終導(dǎo)致數(shù)據(jù)庫崩潰,從而波及整個(gè)系統(tǒng)不可用,好像雪崩一樣。

下面就講講案例,并提供解決方案

常規(guī)寫法

平常的寫法,未考慮異常時(shí)
現(xiàn)在是有一個(gè)查詢商戶的接口
這個(gè)是正常的,結(jié)合了redis的邏輯,

public TbShopEntity rawQuery(Long id) {
        TbShopEntity shop = null;
        //1.是否命中緩存
        String shopJson = redisClient.get(CACHE_SHOP_KEY + id);
        //1.1 如果命中,且數(shù)據(jù)非空,則直接返回
        if (!StrUtil.isEmpty(shopJson)) {
            return JSONObject.parseObject(shopJson, TbShopEntity.class);
        }
        //1.2 查詢數(shù)據(jù)庫
        shop = getById(id);
        //1.2.1 如果數(shù)據(jù)庫不存在,則直接返回
        if (shop == null) return null;
        //1.2.2 如果存在則設(shè)置到redis中
        redisClient.set(CACHE_SHOP_KEY + id, JSON.toJSONString(shop));
        //2.返回?cái)?shù)據(jù)
        return shop;
    }

現(xiàn)在使用一個(gè)不存在的商品id,然后使用jmeter進(jìn)行壓測(cè),例如使用id=0的商品,然后使用200個(gè)線程共發(fā)起200個(gè)請(qǐng)求進(jìn)行壓測(cè)

image.png

結(jié)果:
發(fā)現(xiàn)有大量的請(qǐng)求直接請(qǐng)求數(shù)據(jù)庫,如果時(shí)大量的請(qǐng)求,有可能會(huì)把數(shù)據(jù)庫搞崩,也就是我們的緩存穿透問題

緩存穿透問題

分析:
如果是這種情況,當(dāng)用戶并發(fā)測(cè)試訪問一個(gè)不存在的key時(shí),會(huì)有大量的不存在的key訪問數(shù)據(jù),導(dǎo)致數(shù)據(jù)庫壓力劇增,也就是緩存穿透問題

改進(jìn)方式

緩存穿透一般有兩種解決方式,分別是:

  • 設(shè)置帶有過期時(shí)間的空值
  • 布隆過濾器

設(shè)置帶有過期時(shí)間的空值

邏輯圖如下:主要是改進(jìn)這里

/**
     * 緩存穿透解決方案, 1、設(shè)置一個(gè)空值,且?guī)в休^短的過期時(shí)間 2、布隆過濾器
     * <p>
     * 但是目前還是沒有解決穿透的問題的,因?yàn)樵搆ey不存在,所以會(huì)有多個(gè)請(qǐng)求去直接請(qǐng)求數(shù)據(jù)庫(并發(fā)問題),從而需要進(jìn)行優(yōu)化,解決緩存穿透問題
     */
    private TbShopEntity queryWithPassThrough(Long id) {
        //1.是否命中緩存
        String shopJson = redisClient.get(CACHE_SHOP_KEY + id);
        //1.1 如果命中緩存,則直接返回
        //這里不能使用!Strutil.isEmpty去判斷,因?yàn)樵谙逻厱?huì)使用”“去存空值
        if (shopJson != null) {
            //如果返回的值是"",則直接返回null
            if (StrUtil.isEmpty(shopJson)) return null;
            //否則則返回結(jié)果
            return JSONObject.parseObject(shopJson, TbShopEntity.class);
        }
        //1.2 如果沒有命中,查詢數(shù)據(jù)庫
        TbShopEntity shop = getById(id);
        //1.2.1 如果數(shù)據(jù)庫查詢數(shù)據(jù)不存在,則直設(shè)置值為空,以及過期時(shí)間,現(xiàn)在是設(shè)置為10s,如果10s已經(jīng)過,再重新查詢
        if (shop == null) {
            redisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, StrUtil.EMPTY, SECKILL_SECONDS, TimeUnit.SECONDS);
            return null;
        }
        //1.2.2 如果數(shù)據(jù)庫存在,則設(shè)置到redis中
        redisClient.set(CACHE_SHOP_KEY + id, JSON.toJSONString(shop));
        //并轉(zhuǎn)成shop返回
        //2.返回?cái)?shù)據(jù)
        return JSONObject.parseObject(shopJson, TbShopEntity.class);
    }

測(cè)試一:直接調(diào)用接口
之后進(jìn)行測(cè)試,首先是使用鏈接直接訪問一個(gè)不存在的商品

可以發(fā)現(xiàn),在第一次訪問的時(shí)候,會(huì)去查找數(shù)據(jù)庫,然后在之后的10s內(nèi),都不會(huì)進(jìn)行數(shù)據(jù)庫的查詢了,然后當(dāng)數(shù)據(jù)過期之后,才會(huì)進(jìn)行查詢,從這個(gè)結(jié)果來看,是沒有問題的,也就是平常我們可以這樣子使用的!

測(cè)試二:使用并發(fā)測(cè)試進(jìn)行
但是如果使用200個(gè)并發(fā)去測(cè)試,結(jié)果又是如何呢?

可以發(fā)現(xiàn),還是有大部分請(qǐng)求直接去查詢數(shù)據(jù)庫,

原因是由于線程的并發(fā)問題,大部分請(qǐng)求都執(zhí)行到這一步,這個(gè)時(shí)候redis的數(shù)據(jù)還沒有補(bǔ)充上去,所以導(dǎo)致了大量請(qǐng)求還是重新去查數(shù)據(jù)庫,其實(shí)也類似于緩存穿透問題,當(dāng)某個(gè)key失效時(shí),大量請(qǐng)求會(huì)去查詢數(shù)據(jù)庫,那么這個(gè)問題應(yīng)該如何解決呢?

緩存擊穿問題(其中也解決了穿透問題)

當(dāng)有一個(gè)訪問量較高的key,在失效時(shí),會(huì)導(dǎo)致大量的請(qǐng)求發(fā)向數(shù)據(jù)庫,導(dǎo)致數(shù)據(jù)庫崩潰
解決方式主要有:

  • 加互斥鎖
  • 邏輯過期

加互斥鎖

public TbShopEntity queryWithMutexLock(Long id) {
        TbShopEntity shop = null;
        //是否命中緩存
        String shopJson = redisClient.get(CACHE_SHOP_KEY + id);
        //如果命中緩存,則先判斷緩存是否為”“,如果是返回null,否則返回結(jié)果
        if (shopJson != null) {
            //如果緩存為空的話,則直接返回結(jié)果
            if (StrUtil.isEmpty(shopJson)) return null;
            return JSONObject.parseObject(shopJson, TbShopEntity.class);
        }
        //獲取鎖,鎖的粒度需要精確到id,不能太大
        RLock lock = redissonClient.getLock(LOCK_SHOP + id);
        try {
            //加鎖
            boolean isLock = lock.tryLock(10,TimeUnit.SECONDS);
            //如果沒有獲取到鎖,則休眠50ms,然后重試
            if (!isLock) {
                Thread.sleep(50);
                queryWithMutexLock(id);
            }
            //這里需要做doubleCheck,需要重新查詢緩存的數(shù)據(jù)是否存在,否則還會(huì)出現(xiàn)重復(fù)查詢數(shù)據(jù)庫的情況
            shopJson = redisClient.get(CACHE_SHOP_KEY + id);
            if (shopJson != null) {
                if (StrUtil.isEmpty(shopJson)) return null;
                return JSONObject.parseObject(shopJson, TbShopEntity.class);
            }
            //如果緩存不存在,則查詢數(shù)據(jù)庫
            shop = getById(id);
            //如果數(shù)據(jù)庫查詢數(shù)據(jù)不存在,則直設(shè)置值為空,以及過期時(shí)間,直接返回null
            if (shop == null) {
                redisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, StrUtil.EMPTY, SECKILL_SECONDS, TimeUnit.SECONDS);
                return null;
            }
            //如果數(shù)據(jù)庫數(shù)據(jù)存在則設(shè)置到redis中
            redisClient.set(CACHE_SHOP_KEY + id, JSON.toJSONString(shop));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        //2.返回?cái)?shù)據(jù)
        return shop;
    }

壓測(cè):
使用200個(gè)線程測(cè)試,結(jié)果是只查詢了一次,是ok的

邏輯過期

邏輯過期的原理,指的是不使用redis自帶的expire進(jìn)行存儲(chǔ),而是在存儲(chǔ)的數(shù)據(jù)中,添加一個(gè)過期字段,然后在獲取數(shù)據(jù)的時(shí)候,進(jìn)行該字段的判斷,如果已經(jīng)過期了,則返回舊的數(shù)據(jù),啟動(dòng)一個(gè)線程去更新新的數(shù)據(jù)
數(shù)據(jù)結(jié)構(gòu)如下:

data用來存儲(chǔ)數(shù)據(jù),expired存儲(chǔ)過期時(shí)間,所以我們只要比較,如果expired小于當(dāng)前時(shí)間的話,就代表該數(shù)據(jù)是過期的了

邏輯圖如下:
這個(gè)邏輯圖稍微比較復(fù)雜,基本將空值和互斥鎖都加進(jìn)去了

queryWithLogicExpire

 public TbShopEntity queryWithLogicExpire(Long id) {
        TbShopEntity shop = null;
        //查詢是否命中緩存
        String shopJson = redisClient.get(CACHE_SHOP_KEY + id);
        //如果命中,則判斷結(jié)果是空置,還是過期的值,或者沒過期的值
        if (shopJson != null) {
            //這里的代碼往下滑查看
             return redisDTO2Entity(id, shopJson);
        }
        //獲取鎖,粒度具體到商戶
        RLock lock = redissonClient.getLock("lock:shop:" + id);
        try {
            //加鎖,10s過期
            boolean isLock = lock.tryLock(10,TimeUnit.SECONDS);
            // 如果沒有獲取到鎖,則休眠50ms,然后重試
            if (!isLock) {
                Thread.sleep(50);
                queryWithLogicExpire(id);
            }
            //這里需要做doubleCheck,否則還會(huì)出現(xiàn)重復(fù)查詢數(shù)據(jù)庫的情況
            //這里先不做判斷是否邏輯過期的邏輯
            shopJson = redisClient.get(CACHE_SHOP_KEY + id);
            if (shopJson != null) {
                redisDTO2Entity(id,shopJson);
            }
            shop = getById(id);
            //1.2 如果數(shù)據(jù)庫查詢數(shù)據(jù)不存在,則直設(shè)置值為空,以及過期時(shí)間,直接返回null
            if (shop == null) {
                redisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", SECKILL_SECONDS, TimeUnit.MINUTES);
                return null;
            }
            //1.3 如果存在則設(shè)置到redis中
            redisClient.set(CACHE_SHOP_KEY + id, JSON.toJSONString(shop));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        //2.返回?cái)?shù)據(jù)
        return shop;
    }

redisDTO2Entity

private TbShopEntity redisDTO2Entity(Long id,String shopJson){
        //如果是空值,則直接返回null
        if (StrUtil.isEmpty(shopJson)) return null;
        //或者則轉(zhuǎn)換成redis實(shí)體類
        RedisDTO redisDTO = JSONObject.parseObject(shopJson, RedisDTO.class);
        //獲取邏輯過期時(shí)間
        LocalDateTime expired = redisDTO.getExpired();
        // 判斷時(shí)間是否過期,如果過期,則啟動(dòng)線程更新數(shù)據(jù),其他直接返回
        if (expired.isBefore(LocalDateTime.now())) {
            //使用異步線程,更新數(shù)據(jù)
            saveShop(id);
        }
        return JSONObject.parseObject(JSON.toJSONString(redisDTO.getData()), TbShopEntity.class);
    }

使用異步線程進(jìn)行數(shù)據(jù)的更新
saveShop

    @Async
    public void saveShop(Long id){
        TbShopEntity entity = getById(id);
        if(entity==null) return;
        redisClient.setLogicExpired(RedisConstant.CACHE_SHOP_KEY+id,entity,RedisConstant.SECKILL_SECONDS, TimeUnit.SECONDS);
        log.info("線程{},更新商戶信息",Thread.currentThread().getName());
    }

測(cè)試

需要先添加一條數(shù)據(jù)

執(zhí)行下面的test方法,進(jìn)行添加數(shù)據(jù),添加成功后
數(shù)據(jù)如下:

package com.walker.dianping;
import com.walker.dianping.common.constants.RedisConstant;
import com.walker.dianping.common.utils.RedisClient;
import com.walker.dianping.model.TbShopEntity;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.concurrent.TimeUnit;
@SpringBootTest
public class RedisTest {
    @Autowired
    private RedisClient redisClient;
    @Test
    void addShop() {
        TbShopEntity tbShopEntity = new TbShopEntity();
        tbShopEntity.setId(1L);
        tbShopEntity.setName("邏輯過期測(cè)試商戶");
        //該方法可以查看大綱,放在了完整代碼中
        redisClient.setLogicExpired(RedisConstant.CACHE_SHOP_KEY + 1L, tbShopEntity, 20, TimeUnit.MINUTES);
    }
}

調(diào)用接口發(fā)起測(cè)試

因?yàn)橐婚_始是有的,所以可以直接拿到數(shù)據(jù)

當(dāng)過期時(shí)間expired小于當(dāng)前時(shí)間時(shí),這個(gè)時(shí)候重新去調(diào)用接口(可以直接更改redis的數(shù)據(jù))

這個(gè)時(shí)候就會(huì)去更新商戶的數(shù)據(jù)了,這個(gè)時(shí)候就能拿到新的數(shù)據(jù)了

完整代碼

controller

package com.walker.dianping.controller;
import com.walker.dianping.model.R;
import com.walker.dianping.service.TbShopService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author walker
 * @since 2023-01-18
 */
@RestController
@RequestMapping("/tb-shop-entity")
public class TbShopController {
    @Autowired
    private TbShopService tbShopService;
    /**
    * 獲取商鋪
    */
    @GetMapping("/shop/{id}")
    public R getShop(@PathVariable(value = "id") Long id){
        return tbShopService.getShop(id);
    }
}

service

package com.walker.dianping.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.walker.dianping.model.R;
import com.walker.dianping.model.TbShopEntity;
/**
 * <p>
 *  服務(wù)類
 * </p>
 *
 * @author walker
 * @since 2023-01-18
 */
public interface TbShopService extends IService<TbShopEntity> {
    R getShop(Long id);
}

TbShopServiceImpl

package com.walker.dianping.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.walker.dianping.common.constants.RedisConstant;
import com.walker.dianping.common.utils.RedisClient;
import com.walker.dianping.mapper.TbShopMapper;
import com.walker.dianping.model.R;
import com.walker.dianping.model.TbShopEntity;
import com.walker.dianping.model.dto.RedisDTO;
import com.walker.dianping.service.TbShopService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
import static com.walker.dianping.common.constants.RedisConstant.*;
@Slf4j
@Service
public class TbShopServiceImpl extends ServiceImpl<TbShopMapper, TbShopEntity> implements TbShopService {
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private RedisClient redisClient;
    @Autowired
    private RedissonClient redissonClient;
    @Override
    public R getShop(Long id) {
        //初始查詢
//        TbShopEntity shop = rawQuery(id);
        //緩存穿透
//        TbShopEntity shop = queryWithPassThrough(id);
        //解決緩存擊穿問題
//        TbShopEntity shop = queryWithMutexLock(id);
        //解決緩存擊穿+穿透問題,使用邏輯過期
        TbShopEntity shop = queryWithLogicExpire(id);
        if (shop == null) {
            return R.fail("店鋪不存在");
        }
        return R.ok(shop);
    }
    /**
     * 最初始的版本
     */
    public TbShopEntity rawQuery(Long id) {
        TbShopEntity shop = null;
        //1.是否命中緩存
        String shopJson = redisClient.get(CACHE_SHOP_KEY + id);
        //1.1 如果命中,且數(shù)據(jù)非空,則直接返回
        if (!StrUtil.isEmpty(shopJson)) {
            return JSONObject.parseObject(shopJson, TbShopEntity.class);
        }
        //1.2 查詢數(shù)據(jù)庫
        shop = getById(id);
        //1.2.1 如果數(shù)據(jù)庫不存在,則直接返回
        if (shop == null) return null;
        //1.2.2 如果存在則設(shè)置到redis中
        redisClient.set(CACHE_SHOP_KEY + id, JSON.toJSONString(shop));
        //2.返回?cái)?shù)據(jù)
        return shop;
    }
    /**
     * 緩存穿透解決方案, 1、設(shè)置一個(gè)空值,且?guī)в休^短的過期時(shí)間 2、布隆過濾器
     * <p>
     * 但是目前還是沒有解決穿透的問題的,因?yàn)樵搆ey不存在,所以會(huì)有多個(gè)請(qǐng)求去直接請(qǐng)求數(shù)據(jù)庫(并發(fā)問題),從而需要進(jìn)行優(yōu)化,解決緩存穿透問題
     */
    private TbShopEntity queryWithPassThrough(Long id) {
        //1.是否命中緩存
        String shopJson = redisClient.get(CACHE_SHOP_KEY + id);
        //1.1 如果命中緩存,則直接返回
        //這里不能使用!Strutil.isEmpty去判斷,因?yàn)樵谙逻厱?huì)使用”“去存空值
        if (shopJson != null) {
            //如果返回的值是"",則直接返回null
            if (StrUtil.isEmpty(shopJson)) return null;
            //否則則返回結(jié)果
            return JSONObject.parseObject(shopJson, TbShopEntity.class);
        }
        //1.2 如果沒有命中,查詢數(shù)據(jù)庫
        TbShopEntity shop = getById(id);
        //1.2.1 如果數(shù)據(jù)庫查詢數(shù)據(jù)不存在,則直設(shè)置值為空,以及過期時(shí)間,現(xiàn)在是設(shè)置為10s,如果10s已經(jīng)過,再重新查詢
        if (shop == null) {
            redisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, StrUtil.EMPTY, SECKILL_SECONDS, TimeUnit.SECONDS);
            return null;
        }
        //1.2.2 如果數(shù)據(jù)庫存在,則設(shè)置到redis中
        redisClient.set(CACHE_SHOP_KEY + id, JSON.toJSONString(shop));
        //并轉(zhuǎn)成shop返回
        //2.返回?cái)?shù)據(jù)
        return JSONObject.parseObject(shopJson, TbShopEntity.class);
    }
    /**
     * 緩存擊穿,key失效,導(dǎo)致高并發(fā)的請(qǐng)求
     * 加鎖:可以使用redisson
     */
    public TbShopEntity queryWithMutexLock(Long id) {
        TbShopEntity shop = null;
        //是否命中緩存
        String shopJson = redisClient.get(CACHE_SHOP_KEY + id);
        //如果命中緩存,則先判斷緩存是否為”“,如果是返回null,否則返回結(jié)果
        if (shopJson != null) {
            //如果緩存為空的話,則直接返回結(jié)果
            if (StrUtil.isEmpty(shopJson)) return null;
            return JSONObject.parseObject(shopJson, TbShopEntity.class);
        }
        //獲取鎖,鎖的粒度需要精確到id,不能太大
        RLock lock = redissonClient.getLock(LOCK_SHOP + id);
        try {
            //加鎖
            boolean isLock = lock.tryLock(10,TimeUnit.SECONDS);
            //如果沒有獲取到鎖,則休眠50ms,然后重試
            if (!isLock) {
                Thread.sleep(50);
                queryWithMutexLock(id);
            }
            //這里需要做doubleCheck,需要重新查詢緩存的數(shù)據(jù)是否存在,否則還會(huì)出現(xiàn)重復(fù)查詢數(shù)據(jù)庫的情況
            shopJson = redisClient.get(CACHE_SHOP_KEY + id);
            if (shopJson != null) {
                if (StrUtil.isEmpty(shopJson)) return null;
                return JSONObject.parseObject(shopJson, TbShopEntity.class);
            }
            //如果緩存不存在,則查詢數(shù)據(jù)庫
            shop = getById(id);
            //如果數(shù)據(jù)庫查詢數(shù)據(jù)不存在,則直設(shè)置值為空,以及過期時(shí)間,直接返回null
            if (shop == null) {
                redisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, StrUtil.EMPTY, SECKILL_SECONDS, TimeUnit.SECONDS);
                return null;
            }
            //如果數(shù)據(jù)庫數(shù)據(jù)存在則設(shè)置到redis中
            redisClient.set(CACHE_SHOP_KEY + id, JSON.toJSONString(shop));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        //2.返回?cái)?shù)據(jù)
        return shop;
    }
    private TbShopEntity redisDTO2Entity(Long id,String shopJson){
        //如果是空值,則直接返回null
        if (StrUtil.isEmpty(shopJson)) return null;
        //或者則轉(zhuǎn)換成redis實(shí)體類
        RedisDTO redisDTO = JSONObject.parseObject(shopJson, RedisDTO.class);
        //獲取邏輯過期時(shí)間
        LocalDateTime expired = redisDTO.getExpired();
        // 判斷時(shí)間是否過期,如果過期,則啟動(dòng)線程更新數(shù)據(jù),其他直接返回
        if (expired.isBefore(LocalDateTime.now())) {
            //使用異步線程,更新數(shù)據(jù)
            saveShop(id);
        }
        return JSONObject.parseObject(JSON.toJSONString(redisDTO.getData()), TbShopEntity.class);
    }
    /**
     * 緩存擊穿解決方式二:邏輯過期
     */
    public TbShopEntity queryWithLogicExpire(Long id) {
        TbShopEntity shop = null;
        //查詢是否命中緩存
        String shopJson = redisClient.get(CACHE_SHOP_KEY + id);
        //如果命中,則判斷結(jié)果是空置,還是過期的值,或者沒過期的值
        if (shopJson != null) {
             return redisDTO2Entity(id, shopJson);
        }
        //獲取鎖,粒度具體到商戶
        RLock lock = redissonClient.getLock("lock:shop:" + id);
        try {
            //加鎖,10s過期
            boolean isLock = lock.tryLock(10,TimeUnit.SECONDS);
            // 如果沒有獲取到鎖,則休眠50ms,然后重試
            if (!isLock) {
                Thread.sleep(50);
                queryWithLogicExpire(id);
            }
            //這里需要做doubleCheck,否則還會(huì)出現(xiàn)重復(fù)查詢數(shù)據(jù)庫的情況
            //這里先不做判斷是否邏輯過期的邏輯
            shopJson = redisClient.get(CACHE_SHOP_KEY + id);
            if (shopJson != null) {
                redisDTO2Entity(id,shopJson);
            }
            shop = getById(id);
            //1.2 如果數(shù)據(jù)庫查詢數(shù)據(jù)不存在,則直設(shè)置值為空,以及過期時(shí)間,直接返回null
            if (shop == null) {
                redisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", SECKILL_SECONDS, TimeUnit.MINUTES);
                return null;
            }
            //1.3 如果存在則設(shè)置到redis中
            redisClient.set(CACHE_SHOP_KEY + id, JSON.toJSONString(shop));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        //2.返回?cái)?shù)據(jù)
        return shop;
    }
    @Async
    public void saveShop(Long id) {
        //查詢數(shù)據(jù)
        TbShopEntity entity = getById(id);
        if (entity == null) return;
        redisClient.setLogicExpired(RedisConstant.CACHE_SHOP_KEY + id, entity, RedisConstant.SECKILL_SECONDS, TimeUnit.SECONDS);
        log.info("線程{},更新商戶信息", Thread.currentThread().getName());
    }
}

RedisClient代碼

package com.walker.dianping.common.utils;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.walker.dianping.model.dto.RedisDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.convert.RedisData;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
public class RedisClient {
    @Autowired
    private StringRedisTemplate redisTemplate;
    /**
    * 定義set方法
    */
    public void set(String key,Object value){
        redisTemplate.opsForValue().set(key, JSON.toJSONString(value));
    }
    /**
     * 定義set方法
     */
    public void setLogicExpired(String key,Object value,long timeout, TimeUnit unit){
        RedisDTO dto = new RedisDTO();
        dto.setData(value);
        dto.setExpired(LocalDateTime.now().plusSeconds(unit.toSeconds(timeout)));
        redisTemplate.opsForValue().set(key, JSON.toJSONString(dto));
    }
    /**
    * get方法
    */
    public String get(String key){
        String s = redisTemplate.opsForValue().get(key);
        return s;
    }
}

RedisConstant

package com.walker.dianping.common.constants;
public interface RedisConstant {
    String SECKILL_LUA_SCRIPT="seckill.lua";
    String CACHE_SHOP_KEY= "cache:shop:";
    Integer SECKILL_SECONDS=10;
    String LOCK_SHOP="lock:shop:";
}

到此這篇關(guān)于springboot項(xiàng)目redis緩存異常實(shí)戰(zhàn)案例詳解(提供解決方案)的文章就介紹到這了,更多相關(guān)springboot redis緩存異常內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 關(guān)于Redis的讀寫一致問題

    關(guān)于Redis的讀寫一致問題

    在項(xiàng)目使用Redis過程中,當(dāng)數(shù)據(jù)更新時(shí),我們要保證緩存和數(shù)據(jù)庫的一致性,否則會(huì)導(dǎo)致很多臟數(shù)據(jù)出現(xiàn),此時(shí)我們就要思考如何去進(jìn)行數(shù)據(jù)更新,本文就給大家講講關(guān)于redis的讀寫一致問題,需要的朋友可以參考下
    2023-08-08
  • redis適合場(chǎng)景八點(diǎn)總結(jié)

    redis適合場(chǎng)景八點(diǎn)總結(jié)

    在本篇文章中我們給大家整理了關(guān)于redis適合什么場(chǎng)景的8點(diǎn)知識(shí)點(diǎn)內(nèi)容,需要的朋友們參考下。
    2019-06-06
  • Redis緩存IO模型的演進(jìn)教程示例精講

    Redis緩存IO模型的演進(jìn)教程示例精講

    這篇文章主要為大家介紹了Redis線程IO模型演進(jìn)的教程示例精講,有需要朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪
    2021-11-11
  • 64位Windows下安裝Redis教程

    64位Windows下安裝Redis教程

    這篇文章主要介紹了64位Windows下安裝Redis教程,本文使用Microsoft Open Tech group 在 GitHub上開發(fā)的一個(gè)Win64版本的Redis,需要的朋友可以參考下
    2014-09-09
  • 詳解redis集群選舉機(jī)制

    詳解redis集群選舉機(jī)制

    這篇文章主要介紹了詳解redis集群選舉機(jī)制,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-03-03
  • redis集群實(shí)現(xiàn)清理前綴相同的key

    redis集群實(shí)現(xiàn)清理前綴相同的key

    這篇文章主要介紹了redis集群實(shí)現(xiàn)清理前綴相同的key,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-10-10
  • Redis Scan命令的基本使用方法

    Redis Scan命令的基本使用方法

    這篇文章主要給大家介紹了關(guān)于Redis中Scan命令的基本使用方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Redis具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-12-12
  • Redis實(shí)現(xiàn)數(shù)據(jù)的交集、并集、補(bǔ)集的示例

    Redis實(shí)現(xiàn)數(shù)據(jù)的交集、并集、補(bǔ)集的示例

    本文主要介紹了Redis實(shí)現(xiàn)數(shù)據(jù)的交集、并集、補(bǔ)集的示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-08-08
  • Redis序列化反序列化不一致導(dǎo)致String類型值多了雙引號(hào)問題

    Redis序列化反序列化不一致導(dǎo)致String類型值多了雙引號(hào)問題

    這篇文章主要介紹了Redis序列化反序列化不一致導(dǎo)致String類型值多了雙引號(hào)問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2024-08-08
  • Redis實(shí)現(xiàn)分布式事務(wù)的示例

    Redis實(shí)現(xiàn)分布式事務(wù)的示例

    Redis雖不支持傳統(tǒng)SQL數(shù)據(jù)庫ACID特性的事務(wù),但提供了事務(wù)特性,允許多命令捆綁執(zhí)行,通過命令MULTI、EXEC、DISCARD、WATCH實(shí)現(xiàn),感興趣的可以了解一下
    2024-10-10

最新評(píng)論