本文主要是介绍springboot项目redis缓存异常实战案例详解(提供解决方案),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
《springboot项目redis缓存异常实战案例详解(提供解决方案)》redis基本上是高并发场景上会用到的一个高性能的key-value数据库,属于nosql类型,一般用作于缓存,一般是结合数据...
缓存异常实践案例
Redis基本上是高并发场景上会用到的一个高性能的key-value数据库,属于nosql类型,China编程一般用作于缓存,一般是结合数据库一块使用的,但是在使用的过程中可能会出现异常的问题,就是面试常常唠嗑的缓存异常问题
分别是缓存击穿,缓存穿透和雪崩,简单解释如下:
缓存穿透:
就是当用户查询数据时,缓存和数据库该数据都是不存在的,此时如果用户不断的请求,就会不断的查询缓存和数据库,对数据库造成很大压力
缓存击穿:
当热点key过期或者丢失时,大量的请求访问该数据,缓存不存在,就会将大量的请求直接访问数据库,造成数据库有大量连接,存在崩溃风险
缓存雪崩:
是指大量请求在缓存中没有查到数据,直接访问数据库,导致数据库压力增大,最终导致数据库崩溃,从而波及整个系统不可用,好像雪崩一样。
下面就讲讲案例,并提供解决方案
常规写法
平常的写法,未考虑异常时
现在是有一个查询商户的接口
这个是正常的,结合了redis的逻辑,
public TbShopEntity rawQuery(Long id) { TbShopEntity shop = null; //1.是否命中缓存 String shopjson = redisClient.get(CACHE_SHOP_KEY + id); //1.1 如果命中,且数据非空,则直接返回 if (!StrUtil.isEmpty(shopJson)) { return JSONObject.parseobject(shopJson, TbShopEntity.class); } //1.2 查询数据库 shop = getById(id); //1.2.1 如果数据库不存在,则直接返回 if (shop == null) return null; //1.2.2 如果存在则设置到redis中 redisClient.set(CACHE_SHOP_KEY + id, JSON.toJSONString(shop)); //2.返回数据 return shop; }
现在使用一个不存在的商品id,然后使用jmeter进行压测,例如使用id=0的商品,然后使用200个线程共发起200个请求进行压测
结果:
发现有大量的请求直接请求数据库,如果时大量的请求,有可能会把数据库搞崩,也就是我们的缓存穿透问题
缓存穿透问题
分析:
如果是这种情况,当用户并发测试访问一个不存在的key时,会有大量的不存在的key访问数据,导致数据库压力剧增,也就是缓存穿透问题
改进方式
缓存穿透一般有两种解决方式,分别是:
- 设置带有过期时间的空值
- 布隆过滤器
设置带有过期时间的空值
逻辑图如下:主要是改进这里
/** * 缓存穿透解决方案, 1、设置一个空值,且带有较短的过期时间 2、布隆过滤器 * <p> * 但是目前还是没有解决穿透的问题的,因为该key不存在,所以会有多个请求去直接请求数据库(并发问题),从而需要进行优化,解决缓存穿透问题 */ private TbShopEntity queryWithPassThrough(Long id) { //1.是否命中缓存 String shopJson = redisClient.get(CACHE_SHOP_KEY + id); //1.1 如果命中缓存,则直接返回 //这里不能使用!Strutil.isEmpty去判断,因为在下边会使用”“去存空值 if (shopJson != null) { php //如果返回的值是"",则直接返回null if (StrUtil.isEmpty(shopJson)) return null; //否则则返回结果 return JSONObject.parseObject(shopJson, TbShopEntity.class); } //1.2 如果没有命中,查询数据库 TbShopEntity shop = getById(id); //1.2.1 如果数据库查询数据不存在,则直设置值为空,以及过期时间,现在是设置为10s,如果10s已经过,再重新查询 if (shop == null) { redisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, StrUtil.EMPTY, SECKILL_SECONDS, TimeUnit.SECONDS); return null; } //1.2.2 如果数据库存在,则设置到redis中 redisClient.set(CACHE_SHOP_KEY + id, JSON.toJSONString(shop)); //并转成shop返回 //2.返回数据 return JSONObject.parseObject(shopJson, TbShopEntity.class); }
测试一:直接调用接口
之后进行测试,首先是使用链接直接访问一个不存在的商品
可以发现,在第一次访问的时候,会去查找数据库,然后在之后的10s内,都不会进行数据库的查询了,然后当数据过期之后,才会进行查询,从这个结果来看,是没有问题的,也就是平常我们可以这样子使用的!
测试二:使用并发测试进行
但是如果使用200个并发去测试,结果又是如何呢?
可以发现,还是有大部分请求直接去查询数据库,
原因是由于线程的并发问题,大部分请求都执行到这一步,这个时候redis的数据还没有补充上去,所以导致了大量请求还是重新去查数据库,其实也类似于缓存穿透问题,当某个key失效时,大量请求会去查询数据库,那么这个问题应该如何解决呢?
缓存击穿问题(其中也解决了穿透问题)
当有一个访问量较高的key,在失效时,会导致大量的请求发向数据库,导致数据库崩溃
解决方式主要有:
- 加互斥锁
- 逻辑过期
加互斥锁
public TbShopEntity queryWithMutexLock(Long id) { TbShopEntity shop = null; //是否命中缓存 String shopJson = redisClient.get(CACHE_SHOP_KEY + id); //如果命中缓存,则先判断缓存是否为”“,如果是返回null,否则返回结果 if (shopJson != null) { //如果缓存为空的话,则直接返回结果 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,需要重新查询缓存的数据是否存在,否则还会出现重复查询数据库的情况 shopJson = redisClient.get(CACHE_SHOP_KEY + id); if (shopJson != null) { if (StrUtil.isEmpty(shopJson)) return null; return JSONObject.parseObject(shopJson, TbShopEntity.class); } //如果缓存不存在,则查询数据库 shop = getById(id); //如果数据库查询数据不存在,则直设置值为空,以及过期时间,直接返回null if (shop == null) { redisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, StrUtil.EMPTY, SECKILL_SECONDS, TimeUnit.SECONDS); return null; } //如果数据库数据存在则设置到redis中 redisClient.set(CACHE_SHOP_KEY + id, JSON.toJSONString(shop)); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } //2.返回数据 return shop; }
压测:
使用200个线程测试,结果是只查询了一次,是ok的
逻辑过期
逻辑过期的原理,指的是不使用redis自带的expire进行存储,而是在存储的数据中,添加一个过期字段,然后在获取数据的时候,进行该字段的判断,如果已经过期了,则返回旧的数据,启动一个线程去更新新的数据
数据结构如下:
data用来存储数据,expired存储过期时间,所以我们只要比较,如果expired小于当前时间的话,就代表该数据是过期的了
逻辑图如下:
这个逻辑图稍微比较复杂,基本将空值和互斥锁都加进去了
queryWithLogicExpire
public TbShopEntity queryWithLogicExpire(Long id) { TbShopEntity shop = null; //查询是否命中缓存 String shopJson = redisClient.get(CACHE_SHOP_KEY + id); //如果命中,则判断结果是空置,还是过期的值,或者没过期的值 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,否则还会出现重复查询数据库的情况 //这里先不做判断是否逻辑过期的逻辑 shopJson = redisClient.get(CACHE_SHOP_KEY + id); if (shopJson != null) { redisDTO2Entity(id,shopJson); } shop = getById(id); //1.2 如果数据库查询数据不存在,则直设置值为空,以及过期时间,直接返回null if (shop == null) { redisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", SECKILL_SECONDS, TimeUnit.MINUTES); return null; } //1.3 如果存在则设置到redis中 redisClient.set(CACHE_SHOP_KEY + id, JSON.toJSONString(shop)); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } //2.返回数据 return shop; }
redisDTO2Entity
private TbShopEntity redisDTO2Entity(Long id,String shopJson){ //如果是空值,则直接返回null if (StrUtil.isEmpty(shopJson)) return null; //或者则转换成redis实体类 RedisDTO redisDTO = JSONObject.parseObject(shopJson, RedisDTO.class); //获取逻辑过期时间 LocalDateTime expired = redisDTO.getExpired(); // 判断时间是否过期,如果过期,则启动线程更新数据,其他直接返回 if (expired.isBefore(LocalDateTime.now())) { //使用异步线程,更新数据 saveShop(id); } return JSONObject.parseObject(JSON.toJSONString(redisDTO.getData()), TbShopEntity.class); }
使用异步线程进行数据的更新
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()); }
测试
需要先添加一条数据
执行下面的test方法,进行添加数据,添加成功后
数据如下:
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); } }
调用接口发起测试
因为一开始是有的,所以可以直接拿到数据
当过期时间expired小于当前时间时,这个时候重新去调用接口(可以直接更改redis的数据)
这个时候就会去更新商户的数据了,这个时候就能拿到新的数据了
完整代码
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> * 服务类 * </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.externChina编程.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 如果命中,且数据非空,则直接返回 if (!StrUtil.isEmpty(shopJson)) { return JSONObject.parseObject(shopJson, TbShopEntity.class); } //1.2 查询数据库 shop = getById(id); //1.2.1 如果数据库不存在,则直接返回 if (shop == null) return null; //1.2.2 如果存在则设置到redis中 redisClient.set(CACHE_SHOP_KEY + id, JSON.toJSONString(shop)); //2.返回数据 return shop; } /** * 缓存穿透解决方案, 1、设置一个空值,且带有较短的过期时间 2、布隆过滤器 * <p> * 但是目前还是没有解决穿透的问题的,因为该key不存在,所以会有多个请求去直接请求数据库(并发问题),从而需要进行优化,解决缓存穿透问题 */ private TbShopEntity queryWithPassThrough(Long id) { //1.是否命中缓存 String shopJson = redisClient.get(CACHE_SHOP_KEY + id); //1.1 如果命中缓存,则直接返回 //这里不能使用!Strutil.isEmpty去判断,因为在下边会使用”“去存空值 if (shopJson != null) { //如果返回的值是"",则直接返回null if (StrUtil.isEmpty(shopJson)) return null; //否则则返回结果 return JSONObject.parseObject(shopJson, TbShopEntity.class); } //1.2 如果没有命中,查询数据库 TbShopEntity shop = getById(id); //1.2.1 如果数据库查询数据不存在,则直设置值为空,以及过期时间,现在是设置为10s,如果10s已经过,再重新查询 if (shop == null) { redisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, StrUtil.EMPTY, SECKILL_SECONDS, TimeUnit.SECONDS); return null; } //1.2.2 如果数据库存在,则设置到redis中 redisClient.set(CACHE_SHOP_KEY + id, JSON.toJSONString(shop)); //并转成shop返回 //2.返回数据 return JSONObject.parseObject(shopJson, TbShopEntity.class); } /** * 缓存击穿,key失效,导致高并发的请求 * 加锁:可以使用redisson */ public TbShopEntity queryWithMutexLock(Long id) { TbShopEntity shop = null; //是否命中缓存 String shopJson = redisClient.get(CACHE_SHOP_KEY + id); //如果命中缓存,则先判断缓存是否为”“,如果是返回null,否则返回结果 if (shopJson != null) { //如果缓存为空的话,则直接返回结果 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,需要重新查询缓存的数据是否存在,否则还会出现重复查询数据库的情况 shopJson = redisClient.get(CACHE_SHOP_KEY + id); if (shopJson != null) { if (StrUtil.isEmpty(shopJson)) return null; return JSONObject.parseObject(shopJson, TbShopEntity.class); } //如果缓存不存在,则查询数据库 shop = getById(id); //如果数据库查询数据不存在,则直设置值为空,以及过期时间,直接返回null if (shop == null) { redisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, StrUtil.EMPTY, SECKILL_SECONDS, TimeUnit.SECONDS); return null; } //如果数据库数据存在则设置到redis中 redisClient.set(CACHE_SHOP_KEY + id, JSON.toJSONString(shop)); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } //2.返回数据 return shop; } private TbShopEntity redisDTO2Entity(Long id,String shopJson){ //如果是空值,则直接返回null if (StrUtil.isEmpty(shopJson)) return null; //或者则转换成redis实体类 RedisDTO redisDTO = JSONObject.parseObject(shopJson, RedisDTO.class); //获取逻辑过期时间 LocalDateTime expired = redisDTO.getExpired(); // 判断时间是否过期,如果过期,则启动线程更新数据,其他直接返回 if (expired.isBefore(LocalDateTime.now())) { //使用异步线程,更新数据 saveShop(id); } return JSONObject.parseObject(JSON.toJSONString(redisDTO.getData()), TbShopEntity.class); } /** * 缓存击穿解决方式二:逻辑过期 */ javascript public TbShopEntity queryWithLogicExpire(Long id) { TbShopEntity shop = null; //查询是否命中缓存 String shopJson = redisClient.get(CACHE_SHOP_KEY + id); //如果命中,则判断结果是空置,还是过期的值,或者没过期的值 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,否则还会出现重复查询数据库的情况 //这里先不做判断是否逻辑过期的逻辑 shopJson = redisClient.get(CACHE_SHOP_KEY + id); if (shopJson != null) { redisDTO2Entity(id,shopJson); } shop = getById(id); //1.2 如果数据库查询数据不存在,则直设置值为空,以及过期时间,直接返回null if (shop == null) { redisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", SECKILL_SECONDS, TimeUnit.MINUTES); return null; } //1.3 如果存在则设置到redis中 redisClient.set(CACHE_SHOP_KEY + id, JSON.toJSONString(shop)); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } //2.返回数据 return shop; } @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()); } }
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 coChina编程m.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:"; }
到此这篇关于springboot项目redis缓存异常实战案例详解(提供解决方案)的文章就介绍到这了,更多相关springboot redis缓存异常内容请搜索China编程(www.chinasem.cn)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程China编程(www.chinasem.cn)!
这篇关于springboot项目redis缓存异常实战案例详解(提供解决方案)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!