【Redis】缓存穿透、缓存击穿、缓存雪崩的原因及解决方案

news/2024/11/17 0:18:51/

文章目录

  • 一、缓存穿透
    • 1.1 产生原因
    • 1.2 解决方法
      • 接口校验
      • 对空值进行缓存
      • 使用布隆过滤器
      • 实时监控
  • 二、缓存雪崩
    • 2.2 解决方法
      • 将失效时间分散开
      • 给业务添加多级缓存
      • 构建缓存高可用集群
      • 使用锁或者队列的方式
      • 设置缓存标记
  • 三、缓存击穿
    • 3.2 解决方法
      • 使用互斥锁
      • ”提前“使用互斥锁 / 逻辑过期
      • 提前对热点数据进行设置
      • 监控数据,适时调整
    • 3.3 实现
      • 1 互斥锁
      • 测试
      • 2 逻辑过期

一、缓存穿透

1.1 产生原因

客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会访问数据库。导致DB的压力瞬间变大而卡死或者宕机。

  • 大量的高并发的请求打在redis上
  • 这些请求发现redis上并没有需要请求的资源,redis命中率降低
  • 因此这些大量的高并发请求转向DB请求对应的资源
  • DB压力瞬间增大,直接将DB打垮,进而引发一系列“灾害”

缓存穿透发生的场景一般有两类:

  • 原来数据是存在的,但由于某些原因(误删除、主动清理等)在缓存和数据库层面被删除了,但前端或前置的应用程序依旧保有这些数据;
  • 恶意攻击行为,利用不存在的Key或者恶意尝试导致产生大量不存在的业务数据请求。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jACPoikM-1688634946898)(【Redis】缓存穿透、缓存击穿、缓存雪崩的原因及解决方案/image-20230705214531919.png)]

1.2 解决方法

接口校验

类似于用户权限的拦截,对于id = -3872这些无效访问就直接拦截,不允许这些请求到达Redis、DB上。

对空值进行缓存

比如,虽然数据库中没有id = 1022的用户的数据,但是在redis中对他进行缓存(key=1022, value=null),这样当请求到达redis的时候就会直接返回一个null的值给客户端,避免了大量无法访问的数据直接打在DB上。

但需要注意:

  • key设置的过期时间不能太长,防止占用太多redis资源,设置一个合适的TTL,比如两三分钟。
  • 当遇到黑客暴力请求很多不存在的数据时,就需要写入大量的null值到Redis中,可能导致Redis内存占用不足的情况。

使用布隆过滤器

简单的说就是:通过将一个key的hash值分布到一个大的bit数组上面,判断一个key是否存在时只需判断该的hash对应的bit位是否都是1,如果全是1则表示存在,否则不存在。性能很高但可能存在误判:

如果他告诉你不存在,则一定不存在;如果他告诉你存在,则可能不存在。

使用BitMap作为布隆过滤器,将目前所有可以访问到的资源通过简单的映射关系放入到布隆过滤器中(哈希计算),当一个请求来临的时候先进行布隆过滤器的判断,如果有那么才进行放行,否则就直接拦截。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XmeWjaTb-1688634946899)(【Redis】缓存穿透、缓存击穿、缓存雪崩的原因及解决方案/image-20230705220925615.png)]

实时监控

对redis进行实时监控,当发现redis中的命中率下降的时候进行原因的排查,配合运维人员对访问对象和访问数据进行分析查询,从而进行黑名单的设置限制服务(拒绝黑客攻击)。

二、缓存雪崩

当redis中的大量key集体过期,可以理解为Redis中的大部分数据都清空 / 失效了,这时候如果有大量并发的请求来到,Redis就无法进行有效的响应(命中率急剧下降),也会导致DB先生的绝望。

缓存雪崩的场景通常有两个:

  • 大量热点key同时过期
  • 缓存服务故障或宕机

2.2 解决方法

将失效时间分散开

常用且易于实现通过使用自动生成随机数使得key的过期时间TTL是随机的,防止集体过期。

给业务添加多级缓存

使用nginx缓存 + redis缓存 + 其他缓存,不同层使用不同的缓存,可靠性更强。

构建缓存高可用集群

主要针对缓存服务故障的情景,使用Redis集群来提高服务的可用性。

使用锁或者队列的方式

如果查不到就加上排它锁,其他请求只能进行等待,但这种方式可能影响并发量。

设置缓存标记

热点数据可以不考虑失效,后台异步更新缓存,适用于不严格要求缓存一致性的情景。

三、缓存击穿

Redis中的某个热点key过期,但是此时有大量的用户访问该过期key。

可以看成缓存雪崩的一个特殊子集。

比如xxx塌房哩、xxx商品活动,这时候大量用户都在访问该热点事件,但是可能优于某种原因,redis的这个热点key过期了,那么这时候大量高并发对于该key的请求就得不到redis的响应,那么就会将请求直接打在DB服务器上,导致整个DB瘫痪。

3.2 解决方法

使用互斥锁

只有一个请求可以获取到互斥锁,然后到DB中将数据查询并返回到Redis,之后所有请求就可以从Redis中得到响应。【缺点:所有线程的请求需要一同等待】

”提前“使用互斥锁 / 逻辑过期

在value内部设置一个比缓存(Redis)过期时间短的过期时间标识,当异步线程发现该值快过期时,马上延长内置的这个时间,并重新从数据库加载数据,设置到缓存中去。【缺点:不保证一致性,实现相较互斥锁更复杂】

提前对热点数据进行设置

类似于新闻、某博等软件都需要对热点数据进行预先设置在Redis中,或者适当延长Redis中的Key过期时间。

监控数据,适时调整

监控哪些数据是热门数据,实时的调整key的过期时长。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3p28T6My-1688634946899)(【Redis】缓存穿透、缓存击穿、缓存雪崩的原因及解决方案/image-20230705223722624.png)]

3.3 实现

1 互斥锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vebCChYc-1688634946900)(【Redis】缓存穿透、缓存击穿、缓存雪崩的原因及解决方案/image-20230705224115044.png)]

使用setnx作为Redis中的锁。

  • Redis中查询缓存
    • 存在且不为空值,直接返回
    • 为空值(比如“”、0等特殊值),返回失败结果
    • 不存在,获取锁
  • 获取锁失败,等待重试
  • 获取成功,查找MySQL
    • 不存在,Redis存入空值
    • 存在,写入Redis
  • 释放锁,返回结果
    /*** 根据id查找商户,先到redis中找,再到MySQL中找* @param id* @return*/@Overridepublic Result queryShopById(Long id) {// 用String形式存储JSONString shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);// 如果查询结果不为null,直接返回if (StrUtil.isNotBlank(shopJson)) {Shop shop = JSONUtil.toBean(shopJson, Shop.class);return Result.ok(shop);}// 否则Redis中查询结果为空,判断是否为“”if (shopJson != null) {return Result.fail("店铺不存在,请确认id是否正确");}// 尝试获取锁,// 如果没有得到锁,Sleep一段时间if (!tryLock(LOCK_SHOP_KEY + id)) {try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}// 从开始重试return queryShopById(id);}// 获得了锁,从MySQl中查找Shop shop = this.getById(id);// 模拟重建的延时try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}// 不在MySQL中if (shop == null) {// 将空值写入RedisstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 释放锁unLock(LOCK_SHOP_KEY + id);return Result.fail("店铺不存在,请确认id是否正确");}else {// 在MySQL中,存入redisstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);// 释放锁unLock(LOCK_SHOP_KEY + id);return Result.ok(shop);}}public boolean tryLock(String key) {// 尝试获取锁,set成功返回true,否则返回falseBoolean getLock = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);// 避免getLock为null,使用工具类return BooleanUtil.isTrue(getLock);}public void unLock(String key) {stringRedisTemplate.delete(key);}

测试

F:\Jmeter\bin\ApacheJMeter.jar

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Uo5t40vQ-1688634946900)(【Redis】缓存穿透、缓存击穿、缓存雪崩的原因及解决方案/image-20230706153638543.png)]

对应地,MySQL只执行了1次SQL:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6nAJ3Gg8-1688634946900)(【Redis】缓存穿透、缓存击穿、缓存雪崩的原因及解决方案/image-20230706153739245.png)]

2 逻辑过期

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WsYMPuEj-1688634946900)(【Redis】缓存穿透、缓存击穿、缓存雪崩的原因及解决方案/image-20230706160509947.png)]

    /*** 线程池*/private static final ThreadFactory NAMED_THREAD_FACTORY = new ThreadFactoryBuilder().build();private static final ExecutorService POOL = new ThreadPoolExecutor(5, 200,0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(1024), NAMED_THREAD_FACTORY,new ThreadPoolExecutor.AbortPolicy());public Result queryWithExpire(Long id) {String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);// 如果查询结果为null,直接失败if (StrUtil.isBlank(shopJson)) {return Result.fail("您查询的数据不存在,请检查您的输入");}RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);Shop shop = JSONUtil.toBean((JSONObject)redisData.getData(), Shop.class);// 判断缓存是否过期LocalDateTime time = redisData.getExpireTime();// 未过期,直接返回信息if (time.isAfter(LocalDateTime.now())) {return Result.ok(shop);}// 过期,获取互斥锁失败,返回过期信息if (!tryLock(LOCK_SHOP_KEY + id)) {unLock(LOCK_SHOP_KEY + id);return Result.ok(shop);}// 过期,获取互斥锁成功,开启新线程,重建数据库POOL.submit(() -> {this.saveShop2Redis(id, 20L);unLock(LOCK_SHOP_KEY + id);});// 返回过期信息return Result.ok(shop);}/*** 在MySQL中查找id的shop,写入Redis并更新虚拟过期时间* @param id* @param expireSeconds*/private void saveShop2Redis(Long id, Long expireSeconds) {// 获取Shop shop = this.getById(id);// 封装RedisData redisData = new RedisData();redisData.setData(shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));// 写入RedisstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));}

http://www.ppmy.cn/news/754221.html

相关文章

电商扣减库存_电商后台产品经理宝典

作者:清水红牙搬运 by A小蚊子丨ID:xiaowenzileyuan想了解更多,欢迎关注公众号“A小蚊子(xiaowenzileyuan)”,更多精彩内容、知识大礼包等你发现。欢迎将此文分享给更多朋友,大家共同精进电商架构 电商架构(图电商核心模块(图商品中心 管理SKU:最小库存单位管理SPU:…

Kotlin之类型系统

Kotlin之类型系统 可空类型 在任何类型后加“?”表示该变量可为空。val a: Int? null。 安全的调用 使用“?.”进行安全调用。实现方式&#xff1a;仍旧使用if判空。student?.name。 合并运算符 使用“?:”运算符。 val result a ?: 1 非空断言 使用“!!”操作…

是德频谱仪N9020A维修报错维修-安泰维修

近期&#xff0c;有客户送来一台是德N9020A频谱仪&#xff0c;故障表现为报错。工程师接到仪器后&#xff0c;对其进行故障检测。 经过工程师检查后&#xff0c;开机发现自检失败&#xff0c;报错LO Unlock&#xff0c;无基线。 经检测&#xff0c;仪器前端板损坏&#xff0c;造…

【维修类别】

需求分析 维修类别功能界面如下&#xff1a; 维修类别大致就是故障种类&#xff0c;它是丛属班组的&#xff0c;目前系统中在用的班组只有两个【电仪和设备】 除了从属于班组&#xff0c;维修类别还和具体的设备种类有关&#xff0c;&#xff08;比如加弹机&#xff0c;染色机…

维修行业迫切需要O2O?“报修一站通”寻遍上海为各类水货、无主、超保产品对接维修网点

以下文章转自 36氪 http://www.36kr.com/p/205702.html&#xff0c; 同时也可以向一直关心我的朋友解释下过去一年多我的去向。&#xff1a;&#xff09; 城市发展给我们带来了现代化的生活&#xff0c;同时也带来了某些不便。过去手机坏了、冰箱需要加氟时&#xff0c;一出家…

售后服务系统预约工单方便客户报修?

随着生活水平的不断提高&#xff0c;人们的生活方式也在发生着改变&#xff0c;越来越多人喜欢使用电脑或者手机等数码产品。而当遇到故障需要维修的时候&#xff0c;许多人都是通过电话或者网上联系相关维修公司&#xff0c;但因为缺乏经验和能力&#xff0c;有时因为遇到故障…

免费4s店汽车保养维修记录查询的方式

二手车已经越来越多的走进大家的视野&#xff0c;年轻人也不局限于购买新车&#xff0c;反而对性价比更高的二手车情有独钟。那么二手车和汽车4S维修保养记录有什么关系呢&#xff1f;我作为一个多年经验的二手车商来和大家透露一点小秘密。下图&#xff0c;一般的保养记录差不…

Django的数据库操作的游标(cursor)方法

在Django中&#xff0c;数据库操作的游标方法是一种直接与数据库进行交互的方式&#xff0c;它提供了更底层的数据库访问能力。通过游标方法&#xff0c;你可以执行原始的SQL查询、事务处理以及处理大量数据等操作。 Django的数据库游标方法主要通过connection对象来执行&…