springboot項(xiàng)目redis緩存異常實(shí)戰(zhàn)案例詳解(提供解決方案)
緩存異常實(shí)踐案例
redis基本上是高并發(fā)場景上會(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)行壓測,例如使用id=0的商品,然后使用200個(gè)線程共發(fā)起200個(gè)請(qǐng)求進(jìn)行壓測


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

緩存穿透問題
分析:
如果是這種情況,當(dāng)用戶并發(fā)測試訪問一個(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);
}測試一:直接調(diào)用接口
之后進(jìn)行測試,首先是使用鏈接直接訪問一個(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é)果來看,是沒有問題的,也就是平常我們可以這樣子使用的!
測試二:使用并發(fā)測試進(jìn)行
但是如果使用200個(gè)并發(fā)去測試,結(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;
}壓測:
使用200個(gè)線程測試,結(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());
}測試
需要先添加一條數(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("邏輯過期測試商戶");
//該方法可以查看大綱,放在了完整代碼中
redisClient.setLogicExpired(RedisConstant.CACHE_SHOP_KEY + 1L, tbShopEntity, 20, TimeUnit.MINUTES);
}
}調(diào)用接口發(fā)起測試

因?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)文章希望大家以后多多支持腳本之家!
- SpringBoot+Redis實(shí)現(xiàn)分布式緩存的方法步驟
- springBoot整合redis做緩存具體操作步驟
- SpringBoot3.0集成Redis緩存的實(shí)現(xiàn)示例
- SpringBoot整合Redis實(shí)現(xiàn)token緩存
- SpringBoot結(jié)合Redis實(shí)現(xiàn)緩存管理功能
- SpringBoot整合MP通過Redis實(shí)現(xiàn)二級(jí)緩存方式
- SpringBoot中Redis的緩存更新策略詳解
- SpringBoot整合Redis實(shí)現(xiàn)緩存分頁數(shù)據(jù)查詢功能
- SpringBoot使用Redis實(shí)現(xiàn)分布式緩存
相關(guān)文章
Redis高效查詢大數(shù)據(jù)的實(shí)踐與優(yōu)化詳細(xì)指南
Redis 是一種高性能的鍵值存儲(chǔ)數(shù)據(jù)庫,廣泛應(yīng)用于緩存,排行榜,計(jì)數(shù)器等場景,本文將圍繞如何高效查詢Redis中滿足條件的數(shù)據(jù)展開討論,感興趣的小伙伴可以了解下2025-04-04
淺析對(duì)redis?hashtable?的sizemask理解
在?Redis?的哈希表實(shí)現(xiàn)中,index?=?hash?&?dict->ht[0].sizemask?是計(jì)算鍵值對(duì)應(yīng)存儲(chǔ)位置的核心操作,本文給大家介紹redis?hashtable?的sizemask理解,感興趣的朋友一起看看吧2025-03-03
Redis所實(shí)現(xiàn)的Reactor模型設(shè)計(jì)方案
這篇文章主要介紹了Redis所實(shí)現(xiàn)的Reactor模型,本文將帶領(lǐng)讀者從源碼的角度來查看redis關(guān)于reactor模型的設(shè)計(jì),需要的朋友可以參考下2024-06-06
Redis中管道操作pipeline的實(shí)現(xiàn)
RedisPipeline是一種優(yōu)化客戶端與服務(wù)器通信的技術(shù),通過批量發(fā)送和接收命令減少網(wǎng)絡(luò)往返次數(shù),提高命令執(zhí)行效率,本文就來介紹一下Redis中管道操作pipeline的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2025-03-03
Redis權(quán)限和訪問控制的實(shí)現(xiàn)示例
Redis提供了一些機(jī)制來保護(hù)敏感數(shù)據(jù)和限制對(duì)Redis服務(wù)器的訪問,本文主要介紹了Redis權(quán)限和訪問控制的實(shí)現(xiàn)示例,具有一定的參考價(jià)值,感興趣的可以了解一下2023-12-12

