Redis缓存穿透、缓存击穿、缓存雪崩的解决方案

一、背景

不管是实际工作还是面试,这3个问题都是非常常见的,今天我们就好好探讨一下这个三个问题的解决方案

三者的区别:

缓存穿透:查询缓存和数据库都不存在的数据,缓存没有,数据库也没有

缓存击穿:缓存中数据的key过期了,这时候所有请求都到数据库查询,瞬时大量请求击穿数据库

缓存雪崩:缓存雪崩通常发生在大量key同一时间失效,⼤量的请求进来直接打到DB上,影响整个系统,而缓存击穿是针对某一具体的缓存 key 失效而言,影响相对局部。

接下来,我们逐个分析并解决上述问题

二、缓存穿透

缓存穿透指的查询缓存和数据库中都不存在的数据,这样每次请求直接打到数据库,就好像缓存不存在一样。

缓存穿透指的是一个请求查询一个缓存中不存在的数据,而且这个数据也不在后端存储中,导致大量的请求直接访问后端存储,从而增加了后端存储的负载。

通常情况下,大量的缓存穿透问题不太可能由正常的程序行为引起,更可能是由于恶意攻击者的行为。正常的程序通常会经过合理的设计,避免频繁查询不存在的数据,或者在查询不存在的数据时会有一些容错机制或者缓存预热等策略。

为了防止缓存穿透,可以采取一些预防措施,例如:

  1. 数据校验:对请求的数据进行校验,确保数据的完整性和有效性
  2. 缓存空对象:对于不存在的数据,将其缓存起来,并设置一个较短的过期时间,这样可以避免大量请求直接穿透到数据库。
  3. 布隆过滤器:布隆过滤器是一种数据结构,可以快速判断一个元素是否在一个集合中。在缓存系统中,可以使用布隆过滤器来过滤掉非法请求,避免它们穿透到数据库。
  4. 限流和验证:对于频繁出现缓存穿透的请求,可以进行限流,确保不会过多地访问数据库。
  5. 使用互斥锁或分布式锁:在缓存失效时,使用锁机制防止多个线程同时查询数据库,只允许一个线程去数据库查询,其他线程等待查询结果。

1 未做处理出现缓存穿透

代码演示:

@GetMapping("/getGoodsDetailsWithCache")
public GoodsInfo getGoodsDetailsWithCache(@RequestParam(value = "goodsId") Long goodsId) {String goodsInfoCache = stringRedisTemplate.opsForValue().get(RedisConstants.GOODS_INFO + goodsId);if (StringUtils.isNotBlank(goodsInfoCache)) {log.info("getGoodsDetailsWithCache hit cache, goodsId: {}", goodsId);return JSON.parseObject(goodsInfoCache, GoodsInfo.class);}log.info("getGoodsDetailsWithCache request database, goodsId: {}", goodsId);GoodsInfo goodsInfo = goodsInfoMapper.selectByPrimaryKey(goodsId);// 数据也没有if (goodsInfo == null) {log.warn("getGoodsDetailsWithCache data not find, goodsId: {}", goodsId);return null;}// 保存到RedisstringRedisTemplate.opsForValue().set(RedisConstants.GOODS_INFO + goodsId, JSON.toJSONString(goodsInfo), Duration.ofSeconds(300));log.info("getGoodsDetailsWithCache get by mysql, goodsId: {}", goodsId);return goodsInfo;
}

演示同一个不存在的Key大量请求,goodsId = 10000000(数据库不存在)

大量的请求访问数据库,导致数据库的压力剧增加,严重时会出其他的正常的请求无法访问数据库,所以这种问题必须要提前预防

2 数据校验

对一些明显无效的数据进行校验,可以在一定程度上防止黑客恶意仿造数据库中不存在的Key

存在的问题: 攻击者可以轻松找到key的规律,生成符合规律的Key,比较简单,这里就不演示了

3 缓存空对象

对数据库中不存在的对象也缓存一个Null到Redis中,可以解决攻击者采用少量不同的key攻击

存在的问题:如果攻击者仿造大量不同的key,缓存穿透问题没有解决,还导致Redis增加大量的无效Key,影响正常的key

代码演示

@GetMapping("/getGoodsDetailsWithCache1")
public GoodsInfo getGoodsDetailsWithCache1(@RequestParam(value = "goodsId") Long goodsId) {String goodsInfoCache = stringRedisTemplate.opsForValue().get(RedisConstants.GOODS_INFO + goodsId);if (StringUtils.isNotBlank(goodsInfoCache)) {log.info("getGoodsDetailsWithCache hit cache, goodsId: {}", goodsId);return JSON.parseObject(goodsInfoCache, GoodsInfo.class);}if (Objects.equals(goodsInfoCache, "")) {log.info("getGoodsDetailsWithCache hit cache, goodsId: {}", goodsId);return null;}log.info("getGoodsDetailsWithCache request database, goodsId: {}", goodsId);GoodsInfo goodsInfo = goodsInfoMapper.selectByPrimaryKey(goodsId);// 数据也没有if (goodsInfo == null) {log.warn("getGoodsDetailsWithCache data not find, goodsId: {}", goodsId);stringRedisTemplate.opsForValue().set(RedisConstants.GOODS_INFO + goodsId, "", Duration.ofSeconds(120));return null;}// 保存到RedisstringRedisTemplate.opsForValue().set(RedisConstants.GOODS_INFO + goodsId, JSON.toJSONString(goodsInfo), Duration.ofSeconds(300));log.info("getGoodsDetailsWithCache get by mysql, goodsId: {}", goodsId);return goodsInfo;
}
演示不存在同一个Key的大量请求

goodsId = 10000000(数据库不存在)

全部命中cache,并发量也达到了8k+,数据库仍然得到了保护

演示不存在且不同Key的大量请求

大量的请求仍然访问到了数据库,另外Redis中存在大量的空值,所以比什么都不做,导致系统的问题更大

4 布隆过滤器

因为我们是分布式系统,所以选择Redis版本的布隆过滤器,在分布式环境中使用Redis版本会更容易管理和维护

布隆过滤器的安装过程,这里不做重点演示了,如果连接github网络情况比较差的话,非常麻烦,下面推荐一篇博文,我的安装过程比这个遇到的坑更多安装RedisBloom插件_redisbloom下载-CSDN博客

布隆过滤器在java中的使用,这里我尝试通过Spring提供的RedisTemplate,各种尝试均失败了,无法通过RedisTemplate 操作Redis的布隆过滤器,最后无赖放弃了,网上比较推荐的是使用Redisson,所以最后没办法在一个项目中使用了2个Redis组件,不多说开始演示吧

代码演示

初始化布隆过滤器,预计200w的数据,误判率1%,现在有100w的商品ID需要初始化,  需要等待大约5分钟左右初始化完成(真实的应用是要一直维护布隆过滤器的,每新增一件商品都需要往布隆过滤器中添加商品ID)

@Test
public void testRedissonBloom() {RBloomFilter<Long> filter = redissonClient.getBloomFilter("goods_id_bloom_filter");filter.tryInit(2000000, 0.01);List<Long> idList = goodsInfoMapper.selectGoods();int size = idList.size();for (int i = 0; i < size; i++) {filter.add(idList.get(i));if (i % 1000 == 0) {log.info("进度: " + i * 100 / size + "%");}}
}

下面是用于测试商品加入了布隆过滤器解决了缓存穿透问题,这里要注意的是,布隆过滤器请求需要放在缓存请求之后,数据库访问之前,这样不会影响到正常访问缓存中数据的吞吐量,并且同样能够保护到MySQL数据库

@GetMapping("/getGoodsDetailsWithCache2")
public GoodsInfo getGoodsDetailsWithCache2(@RequestParam(value = "goodsId") Long goodsId) {if(!bloomFilter.contains(goodsId)){log.info("bloomFilter not contains, goodsId: {}", goodsId);return null;}String goodsInfoCache = stringRedisTemplate.opsForValue().get(RedisConstants.GOODS_INFO + goodsId);if (StringUtils.isNotBlank(goodsInfoCache)) {log.info("getGoodsDetailsWithCache hit cache, goodsId: {}", goodsId);return JSON.parseObject(goodsInfoCache, GoodsInfo.class);}if (Objects.equals(goodsInfoCache, "")) {log.info("getGoodsDetailsWithCache hit cache, goodsId: {}", goodsId);return null;}log.info("getGoodsDetailsWithCache request database, goodsId: {}", goodsId);GoodsInfo goodsInfo = goodsInfoMapper.selectByPrimaryKey(goodsId);// 数据也没有if (goodsInfo == null) {log.warn("getGoodsDetailsWithCache data not find, goodsId: {}", goodsId);stringRedisTemplate.opsForValue().set(RedisConstants.GOODS_INFO + goodsId, "", Duration.ofSeconds(86400));return null;}// 保存到RedisstringRedisTemplate.opsForValue().set(RedisConstants.GOODS_INFO + goodsId, JSON.toJSONString(goodsInfo), Duration.ofSeconds(86400));log.info("getGoodsDetailsWithCache get by mysql, goodsId: {}", goodsId);return goodsInfo;
}
演示不存在且不同Key的大量请求

大量的请求都被布隆过滤器过滤掉了

演示访问存在且在缓存中的key

结论

从上面的演示结果来看,引入布隆过滤器的确解决了大量不存在且不同Key缓存穿透问题,虽然牺牲部分Redis的性能,但是保护了MySQL的正常访问。

5 限流和验证

限制来自客户端的请求数量和频率,防止过多的请求直接穿透到数据库,这里可以使用Sentinel 框架限制请求,比如通过IP限流,用户ID限流,一般情况下我们系统访问商品详情需要登录才能访问,那么如果一个用户疯狂请求商品详情接口,也就说明这是恶意攻击了,可以直接拦截掉,所以我认为

6 使用互斥锁或分布式锁

在缓存失效时,使用锁机制防止多个线程同时查询数据库,只允许一个线程去数据库查询,其他线程等待查询结果。我认为这种方式也是一种非常不错简单高效的方式,或者另一种变相的方式是,就不演示了

三、缓存击穿

缓存中数据的key过期了,这时候所有请求都到数据库查询,瞬时大量请求击穿数据库,常见的解决方案

  1. 使用互斥锁或分布式锁: 在缓存失效时,只允许一个线程去加载数据到缓存,其他线程需要等待。这样可以避免多个线程同时访问数据库,减轻数据库压力。

  2. 提前设置较长的缓存过期时间: 在设置缓存的过期时间时,可以将其设置得相对较长,避免在短时间内多次发生缓存失效。

  3. 使用二级缓存: 引入两层缓存,第一层是短期缓存,用于解决高并发下的缓存击穿问题,第二层是较长期的缓存,用于存放相对不频繁变更的数据。这样在第一层缓存失效时,可以从第二层缓存中快速获取数据。

  4. 预加载热点数据: 在系统启动或运行过程中,将一些热点数据提前加载到缓存中,防止在短时间内多次发生缓存失效。

1 未做处理出现缓存击穿场景演示

2 添加互斥锁

看代码,我在请求数据之前加了synchronized 互斥锁,达到保护数据库的作用

  @GetMapping("/getGoodsDetailsWithCache4")public GoodsInfo getGoodsDetailsWithCache4(@RequestParam(value = "goodsId") Long goodsId) {String goodsInfoCache = stringRedisTemplate.opsForValue().get(RedisConstants.GOODS_INFO + goodsId);if (StringUtils.isNotBlank(goodsInfoCache)) {log.info("getGoodsDetailsWithCache hit cache, goodsId: {}", goodsId);return JSON.parseObject(goodsInfoCache, GoodsInfo.class);}if (Objects.equals(goodsInfoCache, "")) {log.info("getGoodsDetailsWithCache hit cache, goodsId: {}", goodsId);return null;}GoodsInfo goodsInfo;synchronized (this) {log.info("getGoodsDetailsWithCache request database, goodsId: {}", goodsId);goodsInfo = goodsInfoMapper.selectByPrimaryKey(goodsId);// 数据也没有if (goodsInfo == null) {log.warn("getGoodsDetailsWithCache data not find, goodsId: {}", goodsId);stringRedisTemplate.opsForValue().set(RedisConstants.GOODS_INFO + goodsId, "", Duration.ofSeconds(3));return null;}// 保存到RedisstringRedisTemplate.opsForValue().set(RedisConstants.GOODS_INFO + goodsId, JSON.toJSONString(goodsInfo), Duration.ofSeconds(3));log.info("getGoodsDetailsWithCache get by mysql, goodsId: {}", goodsId);}return goodsInfo;}

使用synchronized锁的确对数据库起到了保护作用,但是会不会降低正常的并发数,接下来我们测试一下正常没有缓存的商品访问看看吞吐量是多少,使用随机10w个商品ID请求,最后测试的吞吐量为388,比不加synchronized锁直接请求数据库800多的吞吐量还是少了一半多

3 提前设置较长的缓存过期时间

在设置缓存的过期时间时,可以将其设置得相对较长,避免在短时间内多次发生缓存失效

4 使用二级缓存

引入两层缓存,第一层是短期缓存,用于解决高并发下的缓存击穿问题,第二层是较长期的缓存,用于存放相对不频繁变更的数据。这样在第一层缓存失效时,可以从第二层缓存中快速获取数据。

5 预加载热点数据

在系统启动或运行过程中,将一些热点数据提前加载到缓存中,防止在短时间内多次发生缓存失效。

四、缓存雪崩

缓存雪崩通常发生在大量key同一时间失效,⼤量的请求进来直接打到DB上,影响整个系统,而缓存击穿是针对某一具体的缓存 key 失效而言,影响相对局部。

  1. 设置合理的过期时间

    在缓存中设置过期时间,避免所有缓存同时过期。可以使用随机的过期时间,分散缓存过期的时间点,减缓雪崩效应。
  2. 使用互斥锁机制

    在缓存中加入互斥锁,保证在缓存失效的情况下,只有一个请求能够重新生成缓存,其他请求等待该请求完成后再获取缓存,避免大量请求同时落到数据库。
  3. 采用多级缓存

    使用多级缓存架构,例如本地缓存、分布式缓存、全局缓存等。即使一个缓存层出现问题,其他层次的缓存仍然可用,降低缓存雪崩的风险。
  4. 预热缓存

    在系统启动或低峰期,预先加载热门数据到缓存中,避免在高峰期大量请求同时访问数据库。这样可以降低缓存失效时对后端资源的冲击。
  5. 使用缓存异步刷新

    在缓存即将过期时,异步地去更新缓存,而不是等到缓存失效时再去重新生成。这样可以确保缓存数据的时效性,并减小因缓存过期导致的并发请求冲击。
  6. 限流和熔断机制

    对访问缓存的请求进行限流和熔断,防止大量请求同时涌入,减轻系统压力,确保系统能够稳定运行。
  7. 监控和报警

    部署监控系统,实时监测缓存的使用情况、命中率、过期情况等,设置相应的报警机制,及时发现并解决潜在的缓存问题。

综合使用上述策略可以有效地降低缓存雪崩的风险,提高系统的稳定性和性能。

五、总结

缓存穿透、缓存击穿、缓存雪崩看似3个问题,实际上还是有一些相通的点,总结一下,如何在一个接口中做到同时预防这3个问题

1 兜底处理--访问数据库加上互斥锁

3个问题都是要解决缓存失效导致数据库访问量突然增大,那么可以使用一个兜底逻辑,那就是对数据库访问加同步锁,这样最坏的情况,很多请求卡在获取锁的位置,正常没有缓存的请求可能会变慢,但是总体的来说,数据库得到了保护,有缓存的用户请求仍然可以正常访问。

2 布隆过滤器--解决缓存穿透问题

布隆过滤器虽然解决缓存穿透问题,但是需要不断维护,必要时需要重新构建

3 缓存过期时间设置随机

分散缓存过期的时间点,可以减缓雪崩效应。

4 预热缓存

比如某些被认为是热点的商品在添加之时就加入到缓存,而不是等到上架或者访问时加入缓存。

5 对缓存命中率进行监控

实时监测缓存的使用情况、命中率、过期情况等,设置相应的报警机制也是很重要的,一旦发现异常,可以通知到开发人员,及时处理问题。

熔断机制

一旦出现大量线程等待或者访问失败,可以启动熔断机制,保护正常的服务不受影响

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/657529.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java入门高频考查基础知识8(腾讯18问1.5万字参考答案)

刷题专栏&#xff1a;http://t.csdnimg.cn/gvB6r Java 是一种广泛使用的面向对象编程语言&#xff0c;在软件开发领域有着重要的地位。Java 提供了丰富的库和强大的特性&#xff0c;适用于多种应用场景&#xff0c;包括企业应用、移动应用、嵌入式系统等。 以下是几个面试技巧&…

使用AKStream对接gb28181

优点&#xff1a;功能比较多&#xff0c;C#开发的&#xff0c;容易修改&#xff0c;内嵌入了zlmk流媒体服务品&#xff0c;启动简单 缺点&#xff1a;sip对摄像头兼容还有问题&#xff0c;大华接入非常不稳定&#xff0c;注册等待时间久&#xff0c;对海康是正常&#xff0c;占…

IntelliJ Idea实用插件推荐

目录 一、插件安装 二、常用插件 A、代码规范 Alibaba Java Coding Guidelines SonarLint B、快捷开发 aiXcoder-AI代码生成 AWS Toolkit-AI代码生成 CodeGeeX-AI代码生成 CodeGlance-代码缩略图 camelCase-格式转换 GsonFormatPlus-json代码生成 Sequence Giagram…

UE4 CustomDepthMobile流程小记

原生UE opaque材质中获取CustomDepth/CustomStencil会报错 在其Compile中调用的函数中没有看到报错逻辑 材质节点的逻辑都没有什么问题&#xff0c;所以看一下报错 在HLSLMaterialTranslator::Translate中 修改之后 mobile流程的不透明材质可以直接获取SceneTexture::customd…

聚焦AI新动能,九州未来与燧弘华创签约!

1月24日&#xff0c;厦门市电子信息与人工智能产业高质量发展大会成功举办。来自电子信息产业、人工智能领域的企业家、专家等近300位嘉宾齐聚一堂&#xff0c;共谋智能基础&#xff0c;共话产业合作&#xff0c;共享发展商机。 会上&#xff0c;九州未来与燧弘华创签署算力租…

anaconda离线安装包的方法

当设备没有网络时&#xff0c;可以使用有网络的设备先下载所需安装包&#xff0c;然后离线拷贝到需要安装的设备&#xff0c;最后安装。 一. 下载所需安装包 下载命令&#xff1a;使用pip download。详细描述参见pip download -h 以"blind-watermark"为例。 pip …

​学者观察 | 区块链技术理论研究与实践观察——中央财经大学朱建明

导语 当下区块链研究成果质量越来越高&#xff0c;技术应用越来越成熟。在现阶段的研究中存在哪些短板需要弥补&#xff0c;如何将研究成果转化为推动数字经济高质量发展的实际应用&#xff0c;区块链技术与其他新技术结合发展将带来哪些新的机遇&#xff1f; 中央财经大学朱…

eduSRC那些事儿-3(命令执行类+越权逻辑类)

点击星标&#xff0c;即时接收最新推文 本文对edusrc挖掘的部分漏洞进行整理&#xff0c;将案例脱敏后输出成文章&#xff0c;不包含0DAY/BYPASS的案例过程&#xff0c;仅对挖掘思路和方法进行相关讲解。 命令执行类 St2命令执行 在电量查询手机管理平台&#xff0c;观察到.do或…

大坑!react+thress.js

2. UI交互界面与Canvas画布叠加 | Three.js中文网 (webgl3d.cn) // canvas画布绝对定位 renderer.domElement.style.position absolute; renderer.domElement.style.top 0px; renderer.domElement.style.left 0px; renderer.domElement.style.zIndex -1; 我按照教程设置了…

Golang的数字签名之旅:crypto/ecdsa库详解

Golang的数字签名之旅&#xff1a;crypto/ecdsa库详解 引言crypto/ecdsa库概览基本功能安装和设置使用场景 ECDSA原理简介椭圆曲线密码学基础ECDSA的工作原理安全性考虑 Golang中ECDSA的实现密钥生成数字签名签名验证 crypto/ecdsa的高级应用性能优化安全性考虑实际应用案例 总…

掌握 Android JNI 基础

写在前面 最近在看一些底层源码&#xff0c;发现 JNI 这块还是有必要系统的看一下&#xff0c;索性就写一写博客&#xff0c;加深加深印象&#x1f37b; 本文重点聊一聊一些干货&#xff0c;避免长篇大论 JNI 概述 JNI 是什么&#xff1f; 定义&#xff1a;Java Native In…

用GPT写PHP框架

参考https://www.askchat.ai?r237422 写一个mvc框架 上面是简单的案例&#xff0c;完整的PHP框架&#xff0c;其核心通常包含以下几个关键组件&#xff1a; 1. 路由&#xff08;Routing&#xff09;&#xff1a;路由组件负责解析请求的URL&#xff0c;并将其映射到相应的控制…

Kotlin快速入门系列9

Kotlin对象表达式和对象声明 对象表达式 有时&#xff0c;我们想要创建一个对当前类有些许修改的对象同时又不想重新声明一个子类。如果是Java&#xff0c;可以用匿名内部类的概念来解决这个问题。kotlin的对象表达式和对象声明就是为了实现这一点(创建一个对某个类做了轻微改…

使用Mysql实现Postgresql中窗口函数row_number的功能

1. 描述 需要根据用户id&#xff0c;查询每个人得分第二高的科目信息 2. 表结构及数据 2.1 表结构 CREATE TABLE t_score (id bigint(20) NOT NULL AUTO_INCREMENT,user_id bigint(20) NOT NULL,score double NOT NULL,subject varchar(100) NOT NULL,PRIMARY KEY (id) ) E…

在 Amazon EKS 上部署生成式 AI 模型

导言 生成式 AI 正在改变企业的运作方式&#xff0c;并加快创新的步伐。总体而言&#xff0c;人工智能正在改变企业利用技术的方式。生成式 AI 技术包括微调和部署大型语言模型&#xff08;LLM&#xff09;&#xff0c;并允许开发人员访问这些模型以执行提示和对话。负责在 Kub…

【String、StringBuffer和StringBuilder的区别及使用场景】

String、StringBuffer和StringBuilder的区别及使用场景 1. String类是不可变的&#xff0c;一旦创建&#xff0c;就不能修改。每次对String进行操作&#xff08;如拼接、替换等&#xff09;&#xff0c;实际上是创建了一个新的String对象。由于String的不可变性&#xff0c;频繁…

使用 Python 的 Matplotlib 库来绘制简单的爱心图案

import matplotlib.pyplot as plt import numpy as npt np.linspace(0, 2*np.pi, 100) x 16 * np.sin(t)**3 y 13 * np.cos(t) - 5 * np.cos(2*t) - 2 * np.cos(3*t) - np.cos(4*t)plt.plot(x, y, r) plt.axis(equal) plt.fill(x, y, r) plt.show()这段代码首先导入了 Matpl…

【java中如何避免死锁及其分析和解决多线程环境下的死锁问题】

java中如何避免死锁及其分析和解决多线程环境下的死锁问题 死锁是在多线程环境中经常遇到的一种问题&#xff0c;可以通过以下方法来避免和解决死锁问题&#xff1a;死锁是多线程环境下常见的问题&#xff0c;它发生在两个或多个线程等待对方释放资源的情况下。为了避免死锁&am…

uniapp H5 touchstart touchend 切换背景会失效,或者没用

uniapp H5 touchstart touchend 切换背景会失效&#xff0c;或者没用 直接上代码 &#xff08;使用 class 以及 hover-class来设置样式&#xff09; class 设置默认的背景图或者样式 hover-class 来设置按下的背景图 或者样式 抬起 按下 <view class"mp_zoom_siz…

NRF24L01无线 2.4G射频模块(学习笔记)

一、市场上的NRF24L01模块有三种 二、模块的引脚接口 标准的4线SPI接口 三、寄存器操作命令以及寄存器地址 四、两个NRF24L01模块能够成功通信需要满足的条件 五、两个NRF24L01模块通信连接示意图