day2_redis缓存
创始人
2024-04-04 10:47:54
0

文章目录

  • 缓存穿透
  • 缓存雪崩
  • 缓存击穿

缓存穿透

所谓的缓存穿透,是指要查询的数据既不在缓存中,也不再数据库中,此时就会导致缓存永远失效,并且请求都会达到数据库中,从而增加了数据库的查询次数

这时候我们主要有以下几种方式来解决:
返回一个空对象: 当发现数据库中找不到这个数据的时候,那么为了避免下一次再次请求这个数据的时候,还需要访问数据库,这时候我们将一个空串保存到redis中,这样就可以保证了下一次可以从缓存中取出并判断出这个数据并不存在数据库中。

布隆过滤:也就是在从缓存中查询数据之前,会先通过布隆过滤器来进行查询,判断这个数据是否存在,如果不存在,那么就直接告诉前端,提示数据不存在,否则就查询缓存,如果缓存中也不存在,那么就需要取查询数据库了。

所以这里我们将采用返回一个空对象的方式,来解决该项目中的缓存穿透问题, 对应的步骤为:
在这里插入图片描述
所以对应的代码为:

@Overridepublic Result queryShopById(Long id) {String key = RedisConstants.CACHE_SHOP_KEY + id;//1、查询redis中的数据String jsonString = stringRedisTemplate.opsForValue().get(key);if(StringUtils.isNotBlank(jsonString)){//isNotBlank :表示只有在字符串不为null,并且长度不为0,并且不含有空白字符的时候,才返回true,否则返回false//2、redis中店铺存在,直接返回return Result.ok(JSONUtil.toBean(jsonString, Shop.class));}if(jsonString != null && jsonString.length() <= 0){//如果jsonString不等于null,并且长度为0,说明数据库中不存在这个数据//但是在缓存中保存这个空串,从而防止缓存穿透return Result.fail("店铺不存在");}//3、店铺不存在,查询数据库Shop shop = getById(id);if(shop == null){//3.1查询数据为空,将空数据保存到缓存中stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);//3.2 返回错误信息return Result.fail("店铺不存在");}//3.3 将数据保存到redis中stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);//4、将数据返回return Result.ok(shop);}

缓存雪崩

所谓的缓存雪崩,就是在同一个时间内,大量的缓存key同时失效,或者服务器发生了宕机(此时所有的缓存key都已经失效了),此时所有的请求都会到达数据库,从而增加了数据库的查询次数

解决方案主要有以下几种:
① 设置缓存key的TTL为一个随机数,从而避免大量的key在同一个时间失效。例如在批量设置key的TTL的时候,我们在设置的TTL的基础上,加一个随机数,从而避免了各个key的TTL相同的情况。也即最终key的TTL等于 原来的TTL + 随机数.
② 要解决服务器发生宕机这种情况,则需要利用集群来解决
③ 给缓存服务添加多级缓存
④给缓存服务添加降级限流策略

缓存击穿

所谓的缓存击穿,也叫做热点问题,指的是一个被高并发访问,并且缓存重建业务比较复杂的key突然失效,无数的请求都将到达数据库,此时就会导致数据库的访问次数增多, 如下所示:
在这里插入图片描述
所以对应的解决方法主要有以下2中方式:
① 互斥锁: 如上面所示,当线程1在得知缓存没有命中的时候,线程1就去查询数据库,然后重建缓存数据,之后再写入缓存,由于重建缓存业务复杂,所以花费的时间相对较长,这时候如果有线程2,3,4也要查询这个数据,那么他们也需要取查询数据库了。所以我们只需要在线程1执行查询数据库操作之前,设置一个互斥锁,如果线程1能够获得这个互斥锁,那么就可以取执行查询数据库,并且重建缓存业务操作,否则,如果线程2,3,4没有获得互斥锁,则需要睡眠一段时间之后,再次查询缓存是否命中,当线程1执行了操作之后,就需要将互斥锁释放,而这时候的线程2,3,4都可以查询到缓存了,从而解决了缓存击穿的问题, 对应的步骤如下所示:
在这里插入图片描述
对应的代码为:

/*** 通过互斥锁的方式来解决缓存击穿问题,对应的步骤为:* 1、查询缓存,判断是否可以命中,如果可以命中,那么将数据返回* 2、不能命中,那么调用方法tryLock,从而判断是否可以获得互斥锁* 3、如果能够获得互斥锁,那么这个线程就去查询数据库,并且将重建缓存业务操作* 4、否则,如果不能获得互斥锁,那么说明已经有其他线程去查询数据库,执行重建* 缓存业务操作了,此时这个线程只需要睡眠一段时间,然后再次查询缓存即可* 5、当重建缓存业务操作完成之后,就需要释放互斥锁* 6、返回数据* @param id* @return*/public Shop queryWithMutex(Long id){String key = RedisConstants.CACHE_SHOP_KEY + id;//1、查询redis中的数据String jsonString = stringRedisTemplate.opsForValue().get(key);if(StringUtils.isNotBlank(jsonString)){//isNotBlank :表示只有在字符串不为null,并且长度不为0,并且不含有空白字符的时候,才返回true,否则返回false//2、redis中店铺存在,直接返回return JSONUtil.toBean(jsonString, Shop.class);}if(jsonString != null && jsonString.length() <= 0){//如果jsonString不等于null,并且长度为0,说明数据库中不存在这个数据//但是在缓存中保存这个空串,从而防止缓存穿透return null;}//3、店铺不存在,判断是否可以获取互斥锁String lockKey = RedisConstants.LOCK_SHOP_KEY + id;//每个商铺都有一个互斥锁Boolean isLock = tryLock(lockKey);try{if(!isLock){//不能获得互斥锁,将这个线程睡眠一段时间,然后重新查询缓存Thread.sleep(50);return queryWithMutex(id);}//为了保证重建业务相对较长,所以将这个线程睡眠200msThread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}//4、能够获得互斥锁,查询数据库Shop shop = getById(id);if(shop == null){//4.1查询数据为空,将空数据保存到缓存中stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);//4.2 返回nullreturn null;}//4.3 将数据保存到redis中stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);//5、释放互斥锁releaseLock(lockKey);//6、将数据返回return shop;}/*** 判断是否可以获取互斥锁,此时只需要通过setnx这个方法来实现即可* 如果这个方法能够成功执行,说明可以获取互斥锁,否则不可以。* 因为setnx表示的是:只有这个key不存在,才可以添加这个key,否则不可以* 返回true,表示可以执行这个方法,表示可以获得互斥锁, 否则不可以获得互斥锁* @param lockKey* @return*/public Boolean tryLock(String lockKey){//这时候的setIfAbsent可能返回的是null,所以需要通过BooleanUtils来解决Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", RedisConstants.LOCK_SHOP_TTL, TimeUnit.MINUTES);//判断redis是否已经存入了这个key,虽然可以打印出来,但是在客户端中却看不到这个key//System.out.println("redis 中lockKey得知为: " + stringRedisTemplate.opsForValue().get(lockKey));return BooleanUtil.isTrue(flag);}/*** 释放互斥锁* @param lockKey*/public void releaseLock(String lockKey){stringRedisTemplate.delete(lockKey);}

② 逻辑过期: 将数据添加到缓存中的时候,是永不过期的,只是这个key里面的数据多出了一项,来记录它的过期时间。当有线程从缓存中取出这个数据的时候,那么就根据这个key中的这个过期时间,判断是否过期,如果已经过期,那么就另外开一个线程,来进行重建缓存业务操作,而当前这个线程则将没有更新的数据返回给前端,对应的步骤如下所示:
在这里插入图片描述

对应的代码为:

private final ExecutorService service = Executors.newFixedThreadPool(10);/*** 通过逻辑过期来解决缓存击穿问题,对应的步骤为:* 1、查询缓存,是否命中* 2、如果不能命中,说明数据库中并不存在这个数据,直接返回null* 3、如果命中,这时候需要判断逻辑过期时间是否已经过期了* 4、逻辑过期时间还没有到,那么直接将数据返回* 5、逻辑过期时间已经到了,那么判断是否可以获取互斥锁,* 6、可以获取互斥锁,那么就另外开一条线程,用来查询数据库,以及重建缓存* 业务操作* 7、本线程直接将数据返回* @param id* @return*/public Shop queryWithLogicalExpire(Long id){//1、从缓存中取出数据String key = RedisConstants.CACHE_SHOP_KEY + id;String jsonStr = stringRedisTemplate.opsForValue().get(key);if(StringUtils.isBlank(jsonStr)){//数据为空,说明不能命中,说明数据库中并没有这个数据,直接返回nullreturn null;}//2.1 命中,那么获取过期时间,首先将jsonStr转成RedisData类型RedisData redisData = JSONUtil.toBean(jsonStr, RedisData.class);System.out.println("redisData = " + redisData);//2.2 获取逻辑过期时间LocalDateTime expireTime = redisData.getExpireTime();//因为RedisData中的data是一个Object类型,所以当反序列化,也即toBean方法调用之后,//就会将这一串数据的jsonStr变成JSONObject返回,此时就不可以强转成为ShopShop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);//2.3 判断是否已经过了逻辑时间if(expireTime.isAfter(LocalDateTime.now())){//2.4 没有过期,直接将数据返回return shop;}//3 已经过期,那么判断是否可以获得互斥锁String lockKey = RedisConstants.LOCK_SHOP_KEY + id;Boolean isLock = tryLock(lockKey);if(isLock){//3.1 可以得到互斥锁,那么要另外开启一条线程,进行重建缓存任务,//这里通过线程池来完成service.execute(() -> {try{//3.2 重建缓存save2Shop(id, 20L);}catch(RuntimeException e){throw new RuntimeException(e);} catch (InterruptedException e) {e.printStackTrace();} finally {//3.3 释放互斥锁releaseLock(lockKey);}});}//4 将数据返回即可return shop;}public void save2Shop(Long id, Long expiredTime) throws InterruptedException {//1、查询数据库Shop shop = getById(id);Thread.sleep(200);//2、添加到缓存中,并且为了解决缓存击穿的问题,需要添加一个逻辑过期时间RedisData redisData = new RedisData();redisData.setData(shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(expiredTime));stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));}

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
客厅放八骏马摆件可以吗(家里摆... 今天给各位分享客厅放八骏马摆件可以吗的知识,其中也会对家里摆八骏马摆件好吗进行解释,如果能碰巧解决你...
苏州离哪个飞机场近(苏州离哪个... 本篇文章极速百科小编给大家谈谈苏州离哪个飞机场近,以及苏州离哪个飞机场近点对应的知识点,希望对各位有...