IT评测·应用市场-qidao123.com

标题: day03-商家查询缓存02 [打印本页]

作者: 东湖之滨    时间: 2023-4-20 23:20
标题: day03-商家查询缓存02
功能02-商铺查询缓存02

知识补充

(1)缓存穿透

https://blog.csdn.net/qq_45637260/article/details/125866738
缓存穿透(cache penetration)是指用户访问的数据既不在缓存当中,也不在数据库中。出于容错的考虑,如果从底层数据库查询不到数据,则不写入缓存。这就导致每次请求都会到底层数据库进行查询,缓存也失去了意义。当高并发或有人利用不存在的Key频繁攻击时,数据库的压力骤增,甚至崩溃,这就是缓存穿透问题。
简单地说,缓存穿透是指用户请求的数据在缓存和数据库中都不存在,则每次请求都会打到数据库中,给数据库带来巨大压力。
常见的两种解决方案
(1)缓存空对象:是指在持久层没有命中的情况下,对key进行set (key,null)。
缓存空对象会有两个问题:
(2)布隆过滤器:
在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截,当收到一个对key请求时,先用布隆过滤器验证是key否存在,如果存在再进入缓存层、存储层。
可以使用bitmap做布隆过滤器。这种方法适用于数据命中不高、数据相对固定、实时性低的应用场景,代码维护较为复杂,但是缓存空间占用少。
布隆过滤器实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
布隆过滤器拦截的算法描述:
初始状态时,BloomFilter是一个长度为m的位数组,每一位都置为0。添加元素x时,x使用k个hash函数得到k个hash值,对m取余,对应的bit位设置为1。
判断y是否属于这个集合,对y使用k个哈希函数得到k个哈希值,对m取余,所有对应的位置都是1,则认为y属于该集合(哈希冲突,可能存在误判),否则就认为y不属于该集合。可以通过增加哈希函数和增加二进制位数组的长度来降低错报率
两种方案的比较:
缓存穿透的方案使用场景维护成本缓存空对象1.数据命中率不高 2.数据频繁变化实时性高1.代码维护简单 2.需要过多的缓存空间 3.数据不一致布隆过滤器1.数据命中不高 2.数据相对固定实时性低1.代码维护复杂 2.缓存空间占用少缓存穿透的解决方案还有:
(2)缓存雪崩

缓存雪崩
在使用缓存时,通常会对缓存设置过期时间,一方面目的是保持缓存与数据库数据的一致性,另一方面是减少冷缓存占用过多的内存空间。但当缓存中大量热点缓存采用了相同的实效时间,就会导致缓存在某一个时刻同时实效,请求全部转发到数据库,从而导致数据库压力骤增,甚至宕机。从而形成一系列的连锁反应,造成系统崩溃等情况,这就是缓存雪崩(Cache Avalanche)。
简单地说,缓存雪崩是指在同一时间段大量的热点key同时失效,或者Redis服务宕机,导致大量请求到达数据库,给数据库带来巨大压力。
解决方案
(3)缓存击穿

缓存击穿
如果有一个热点key,在不停的扛着大并发,在这个key失效的瞬间,持续的大并发请求就会击破缓存,直接请求到数据库,好像蛮力击穿一样。这种情况就是缓存击穿(Cache Breakdown)。
缓存击穿问题也叫做热点key问题,简单来说,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问在瞬间给数据库带来巨大的冲击。
从定义上可以看出,缓存击穿和缓存雪崩很类似,只不过是缓存击穿是一个热点key失效,而缓存雪崩是大量热点key失效。因此,可以将缓存击穿看作是缓存雪崩的一个子集。
解决方案
方案一:使用互斥锁(Mutex Key),只让一个线程构建缓存,其他线程等待构建缓存执行完毕,重新从缓存中获取数据。单机通过synchronized或lock来处理,分布式环境采用分布式锁。
方案二:逻辑过期。热点数据不设置过期时间,只在value中设置逻辑上的过期时间。后台异步更新缓存,适用于不严格要求缓存一致性的场景。
两种方案的对比:
3.功能02-商铺查询缓存

3.4查询商铺id的缓存穿透问题

3.4.3需求分析

解决查询商铺查询可能存在的缓存穿透问题:当访问不存在的店铺时,请求会直接打到数据库上,并且redis缓存永远不会生效。
这里使用缓存空对象的方式来解决。
3.4.4代码实现

(1)修改ShopServiceImpl.java的queryById方法
  1. @Override
  2. public Result queryById(Long id) {
  3.     String key = CACHE_SHOP_KEY + id;
  4.     //1.从redis中查询商铺缓存
  5.     String shopJson = stringRedisTemplate.opsForValue().get(key);
  6.     //2.判断缓存是否命中
  7.     if (StrUtil.isNotBlank(shopJson)) {
  8.         //2.1若命中,直接返回商铺信息
  9.         Shop shop = JSONUtil.toBean(shopJson, Shop.class);
  10.         return Result.ok(shop);
  11.     }
  12.     //判断命中的是否是redis的空值
  13.     if (shopJson != null) {
  14.         return Result.fail("店铺不存在!");
  15.     }
  16.     //2.2未命中,根据id查询数据库,判断商铺是否存在数据库中
  17.     Shop shop = getById(id);
  18.     if (shop == null) {
  19.         //2.2.1不存在,防止缓存穿透,将空值存入redis,TTL设置为2min
  20.         stringRedisTemplate.opsForValue().set(key, "",
  21.                 CACHE_NULL_TTL, TimeUnit.MINUTES);
  22.         //返回错误信息
  23.         return Result.fail("店铺不存在!");
  24.     }
  25.     //2.2.2存在,则将商铺数据写入redis中
  26.     stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),
  27.             CACHE_SHOP_TTL, TimeUnit.MINUTES);
  28.     return Result.ok(shop);
  29. }
复制代码
(2)测试,访问一个缓存和数据库都不存在的数据:
可以看到redis已经缓存了一个空值

之后再访问该数据,只要redis的空值对没有过期,就不会访问到数据库,从而起到保护数据库的作用。
3.5查询商铺id的缓存击穿问题

当查询店铺id时,可能会出现该店铺id对应的缓存失效,从而大量请求发送到数据库的情况,这里使用两种方案分别解决该问题。
3.5.1基于互斥锁方案解决

3.5.1.1需求分析

修改根据id查询商铺的业务,基于互斥锁方式来解决缓存击穿问题。
如下,当出现缓存击穿问题,首先需要判断当前的线程是否能够获取锁:
根据redis的setnx命令,当setnx设置某个key之后,如果该key存在,则其他线程无法设置该key。
我们可以根据这个特性,作为一个lock的逻辑标志,当一个线程setnx某个key后,代表获取了“锁”。当删除这个key时,代表释放“锁”,这样其他线程就可以重新获取“锁”。此外,可以对该key设置一个有效期,防止删除key失败,产生“死锁”。
3.5.1.2代码实现

(1)修改 ShopServiceImpl.java
  1. package com.hmdp.service.impl;
  2. import ...
  3. /**
  4. * 服务实现类
  5. *
  6. * @author 李
  7. * @version 1.0
  8. */
  9. @Service
  10. public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop>
  11.         implements IShopService {
  12.     @Resource
  13.     StringRedisTemplate stringRedisTemplate;
  14.     @Override
  15.     public Result queryById(Long id) {
  16.         Shop shop = queryWithMutex(id);
  17.         if (shop == null) {
  18.             return Result.fail("店铺不存在!");
  19.         }
  20.         return Result.ok(shop);
  21.     }
  22.     //缓存穿透(存储空对象)+缓存击穿解决(互斥锁解决)
  23.     public Shop queryWithMutex(Long id) {
  24.         String key = CACHE_SHOP_KEY + id;
  25.         //从redis中查询商铺缓存
  26.         String shopJson = stringRedisTemplate.opsForValue().get(key);
  27.         //判断缓存是否命中
  28.         if (StrUtil.isNotBlank(shopJson)) {
  29.             //命中,直接返回商铺信息
  30.             return JSONUtil.toBean(shopJson, Shop.class);
  31.         }
  32.         //判断命中的是否是redis的空值(缓存击穿解决)
  33.         if (shopJson != null) {
  34.             return null;
  35.         }
  36.         //未命中,尝试获取互斥锁
  37.         String lockKey = "lock:shop:" + id;
  38.         boolean isLock = false;
  39.         Shop shop = null;
  40.         try {
  41.             //获取互斥锁
  42.             isLock = tryLock(lockKey);
  43.             //判断是否获取成功
  44.             if (!isLock) {//失败
  45.                 //等待并重试
  46.                 Thread.sleep(50);
  47.                 //直到缓存命中,或者获取到锁
  48.                 return queryWithMutex(id);
  49.             }
  50.             //获取锁成功,开始重建缓存
  51.             //根据id查询数据库,判断商铺是否存在数据库中
  52.             shop = getById(id);
  53.             //模拟重建缓存的延迟-----------
  54.             Thread.sleep(200);
  55.             if (shop == null) {
  56.                 //不存在,防止缓存穿透,将空值存入redis,TTL设置为2min
  57.                 stringRedisTemplate.opsForValue().set(key, "",
  58.                         CACHE_NULL_TTL, TimeUnit.MINUTES);
  59.                 //返回错误信息
  60.                 return null;
  61.             }
  62.             //存在,则将商铺数据写入redis中
  63.             stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),
  64.                     CACHE_SHOP_TTL, TimeUnit.MINUTES);
  65.         } catch (InterruptedException e) {
  66.             throw new RuntimeException(e);
  67.         } finally {
  68.             //释放互斥锁
  69.             unLock(lockKey);
  70.         }
  71.         //返回从缓存或数据库中查到的数据
  72.         return shop;
  73.     }
  74.     //缓存穿透方案
  75. //    public Shop queryWithPassThrough(Long id) {
  76. //        String key = CACHE_SHOP_KEY + id;
  77. //        //1.从redis中查询商铺缓存
  78. //        String shopJson = stringRedisTemplate.opsForValue().get(key);
  79. //        //2.判断缓存是否命中
  80. //        if (StrUtil.isNotBlank(shopJson)) {
  81. //            //2.1若命中,直接返回商铺信息
  82. //            return JSONUtil.toBean(shopJson, Shop.class);
  83. //        }
  84. //        //判断命中的是否是redis的空值
  85. //        if (shopJson != null) {
  86. //            return null;
  87. //        }
  88. //        //2.2未命中,根据id查询数据库,判断商铺是否存在数据库中
  89. //        Shop shop = getById(id);
  90. //        if (shop == null) {
  91. //            //2.2.1不存在,防止缓存穿透,将空值存入redis,TTL设置为2min
  92. //            stringRedisTemplate.opsForValue().set(key, "",
  93. //                    CACHE_NULL_TTL, TimeUnit.MINUTES);
  94. //            //返回错误信息
  95. //            return null;
  96. //        }
  97. //        //2.2.2存在,则将商铺数据写入redis中
  98. //        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),
  99. //                CACHE_SHOP_TTL, TimeUnit.MINUTES);
  100. //        return shop;
  101. //    }
  102.     private boolean tryLock(String key) {
  103.         Boolean flag = stringRedisTemplate.opsForValue()
  104.                 .setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
  105.         return BooleanUtil.isTrue(flag);
  106.     }
  107.     private void unLock(String key) {
  108.         stringRedisTemplate.delete(key);
  109.     }
  110.     @Override
  111.     @Transactional
  112.     public Result update(Shop shop) {
  113.         Long id = shop.getId();
  114.         if (id == null) {
  115.             return Result.fail("店铺id不能为空");
  116.         }
  117.         //1.更新数据库
  118.         updateById(shop);
  119.         //2.删除redis缓存
  120.         stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
  121.         return Result.ok();
  122.     }
  123. }
复制代码
(2)使用jemeter模拟高并发的情况:
5秒发起1000个请求线程:
模拟http请求:
全部请求成功,获取到数据:
在服务器的控制台中可以看到:对于数据库的请求只触发了一次,证明在高并发的场景下,只有一个线程对数据库发起请求,并对redis对应的缓存重新设置。
3.5.2基于逻辑过期方案解决


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!




欢迎光临 IT评测·应用市场-qidao123.com (https://dis.qidao123.com/) Powered by Discuz! X3.4