REDIS中的缓存穿透,缓存击穿,缓存雪崩原因以及解决方案

news2024/11/27 4:14:49
  1. 需求引入

一般在项目的开发中,都是使用关系型数据库来进行数据的存储,通常不会存在什么高并发的情况,可是一旦涉及大数据量的需求,比如商品抢购,网页活动导致的主页访问量瞬间增大,单一使用关系型数据库来保存数据的系统会因为面向磁盘,导致磁盘读/写速度比较慢,一瞬间成千上万的请求到来,系统在极短的时间内完成成千上万次的读/写操作,这个时候数据库一般不能够承受,极其容易造成数据库系统瘫痪,最终导致服务宕机等严重性能弊端,为了解决上述问题,项目通常会引入NoSQL数据库,这是一种基于内存的数据库,并且提供一定的持久化功能,redis就是NoSQL数据库中的一种,可以将 Redis 作为数据库的缓存,用与缓解关系型数据库的压力,但是引入redis有可能会出现缓存穿透,缓存击穿,缓存雪崩等问题
  1. 缓存穿透

(1).什么是缓存穿透

缓存穿透是指查询一个缓存中和数据库中都不存在的数据,导致每次查询这条数据都会发生缓存缺失,从而透过缓存,直接访问数据库,发现数据库中也没有要访问的数据,最后返回空。如果用户使用这条不存在的数据疯狂发起请求,就会同时给缓存和数据库带来巨大压力,从而可能压垮数据库,导致数据库宕机

(2).产生缓存穿透的原因

业务层误操作:缓存中的数据和数据库中的数据都被误删除,所以缓存和数据库中都没有数据
恶意攻击:专门访问数据库中没有的数据

(3).解决方案

1).对请求参数进行校验

对于请求的参数应该要先进行校验,请求的参数应该要在规定范围内,过滤掉一些无效的请求,避免请求进入缓存或者数据库

2).采用布隆过滤器

使用布隆过滤器快速判断数据是否存在,避免从数据库中查询数据是否存在,减轻数据库压力

布隆过滤器,就是一种BitMap数据结构,将目前所有可以访问到的资源通过简单的映射关系放入到布隆过滤器中(哈希计算),当一个请求来临的时候先进行布隆过滤器的判断,如果有那么才进行放行,否则就直接拦截,它是由一个长度为m bit的位数组与n个hash函数组成的数据结构,位数组中每个元素的初始值都是0。在初始化布隆过滤器时,会先将所有key进行n次hash运算,这样就可以得到n个位置,然后将这n个位置上的元素改为1。这样,就相当于把所有的key保存到了布隆过滤器中了。

举个例子,比如我们一共有3个key,我们对这3个key分别进行3次hash运算,key1经过三次hash运算后的结果分别为2/6/10,那么就把布隆过滤器中下标为2/6/10的元素值更新为1,然后再分别对key2和key3做同样操作,结果如下图:

这样,当客户端查询时,也对查询的key做3次hash运算得到3个位置,然后看布隆过滤器中对应位置元素的值是否为1,如果所有对应位置元素的值都为1,就证明key在库中存在,则继续向下查询;如果3个位置中有任意一个位置的值不为1,那么就证明key在库中不存在,直接返回客户端空即可。如下图:

当客户端查询key4时,key4的3次hash运算中,有一个位置8的值为0,就说明key4在库中不存在,直接返回客户端空即可。

所以,布隆过滤器就相当于一个位于客户端与缓存层中间的拦截器一样,负责判断key是否在集合中存在。如下图:

布隆过滤器的好处就是解决了第一种缓存空值的不足,但布隆过滤器也存在缺陷它不能完全保证请求过来的 key ,通过布隆过滤器的校验,就一定有这个数据。 但是,只要没有通过布隆过滤器的校验,那么这个 key 就一定不存在。 其实这样就已经可以过滤掉大部分不存在的 key 请求了。

如果布隆过滤器的哈希槽过短,很有可能导致大部分的位置都为 1 ,那么此时,布隆过滤器就失去了它的意义。 所以,当我们发现布隆过滤器大部分位置都为1了,就要扩宽哈希槽

3).对空值进行缓存

一旦发生缓存穿透,当查询返回的数据为空(不管是数据不存在,还是系统故障)的时候,在Redis缓存一个空值或者业务层协商确定的缺省值,然后给这个空对象的缓存设置一个很短的过期时间,最长不超过五分钟,应用发生后续请求在进行查询时,就可以直接从缓存中拿到返回给业务应用,避免大量请求发送给数据库处理,保持数据库的正常运行

这种解决方式有两个缺点:(1)需要缓存层提供更多的内存空间来缓存这些空对象,当这种空对象很多的时候,就会浪费更多的内存;(2)会导致缓存层和存储层的数据不一致,即使在缓存空对象时给它设置了一个很短的过期时间,那也会导致这一段时间内的数据不一致问题

//伪代码
public function GetShopList() {
    var cache_time = 60;
    var cache_key = "shop_list";
    var cache_value = Cache::Get(cacheKey);

    if (cache_value != null) {
        return cache_value;
    } else {
        //数据库查询不到,为空
        cache_value = GetShopListData();
        if (cache_value == null) {
            //如果发现为空,设置个默认值,也缓存起来
            cache_value = null;
        }
        //增加缓存
        Cache::Add(cache_key, cache_value, cache_time);
        return cache_value;
    }
}

(4).注意事项:

1).使用空值作为缓存的时候,key设置的过期时间不能太长,防止占用太多redis资源
2).使用空值作为缓存只能防止黑客重复使用相同的id暴力攻击,但是如果黑客使用动态的无效id攻击就没有效果(需要配合网警)
3).使用布隆过滤器可能会存在哈希冲突
  1. 缓存击穿

(1).什么是缓存击穿

针对某个访问非常频繁的热点数据请求,突然在缓存中失效(数据过期),在该热点数据重新载入缓存之前,有大量的访问该数据请求穿过缓存,一下子都发送到后端数据库,导致数据库压力激增影响数据库处理其他请求,造成大量请求阻塞,这个时候大并发的请求可能会瞬间把后端DB压垮。缓存击穿一般是热点 key 在 Redis 中过期了导致的,最直接的方法就是,对于热点 key ,不设置过期时间

(2).解决方案

1).第一种是设置key永不过期

a.在设置热点key的时候,不给key设置过期时间即可
b.正常给key设置过期时间,不过在后台同时启一个定时任务去定时地更新这个缓存
c.提前对热点数据进行设置:类似于新闻、某博等软件都需要对热点数据进行预先设置在redis中
d.监控数据,适时调整:监控哪些数据是热门数据,实时的调整key的过期时长

2).第二种是使用分布式锁

锁的对象就是key,当大量查询同一个key的请求并发进来时,保证同一时刻只能有一个请求获取到锁,然后获取到锁的线程查询数据库,然后将结果放入到缓存中,这样,其他的线程只需等待该线程运行完毕,即可重新从Redis中获取数据
业界比较常用的做法,是使用mutex,简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去加载请求数据库,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行加载请求数据库的操作并回设缓存;否则,就重试整个get缓存的方法
SETNX,是SET if Not eXists 的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果
public function get(key) {
   var value = redis.get(key);
   if (value == null) { //代表缓存值过期
          //设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能请求加载数据库
      if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {  //代表设置成功
               value = db.get(key);
               redis.set(key, value, expire_secs);
               redis.del(key_mutex);
              } else {  //这个时候代表同时候的其他线程已经请求加载数据库并回设到缓存了,这时候重试获取缓存值即可
                sleep(50);
                get(key);  //重试
              }
      } else {
              return value;      
      }
 }

4.缓存雪崩

(1).什么是缓存雪崩

与缓存击穿的区别在于缓存雪崩针对的是很多key缓存,缓存击穿则是某一个key,缓存正常从Redis中获取示意图:
缓存失效瞬间示意图:
缓存雪崩指的是,缓存中有大量的key在同一时刻过期,或者Redis直接宕机了,然后大量请求发送到了数据库,导致数据库的压力激增,甚至可能导致数据库崩溃,从而导致整个系统崩溃,引发雪崩一样的连锁效应

(2).产生缓存雪崩的原因

1).缓存中大量 key 同时过期

当redis中的大量key集体过期,可以理解为redis中的大部分数据都被清空了(失效了),那么这时候如果有大量并发的请求来到,那么redis就无法进行有效的响应(命中率急剧下降),请求就直接去数据库访问了,可能导致素剧看直接崩溃

2).Redis 实例挂掉了,无法处理请求

(3).解决方案

a.将失效时间分散开:
在实际应用中应当避免大量 key 同时过期的场景。如果确实有这种业务场景,可以微调这批 key 过期的时间,使其能有一定的相差间隔,防止集体过期
b. 使用多级架构:
使用nginx缓存+redis缓存+其他缓存,不同层使用不同的缓存,可靠性更强,Redis 主从集群其实可以比较好地实现主 Redis 实例挂掉后,能有其他从库快速切换为主库,继续提供服务
c. 设置缓存标记:
记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去跟新实际的key
d. 使用锁或者队列的方式:
如果查不到就加上排它锁,其他请求只能进行等待

以上都是预防的措施,如果已经发生了 缓存雪崩,为了防止数据库被大量的请求搞崩溃,可以采用 服务熔断 或者 请求限流
服务熔断就是暂停对业务提供 Redis 服务,直到 Redis 恢复正常,再向外提供服务。 当然,这种情况下,业务也会整个停摆了。
另外一种比较温和的办法就是请求限流。请求限流顾名思义,就是限制请求的流量,随机丢弃一部分的请求,以保证不会同时有太多请求压入数据库

除了上面的解决方式,还可以使用其他策略,比如设置key永不过期、加分布式锁等

缓存失效时的雪崩效应对底层系统的冲击非常可怕!大多数系统设计者考虑用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。还有一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件

加锁排队,伪代码如下


public function GetList() {
    $cache_time = 30;
    $cache_key = "product_list";
    $lock_key = $cache_key;

    $cache_value = Cache::get($cache_key);
    if ($cache_value != null) {
        return $cache_value;
    } else {
        synchronized($lock_key) {
            $cache_value = Cache::get($cache_key);
            if ($cache_value != null) {
                return $cache_value;
            } else {
              //这里一般是sql查询数据
                $cache_value = GetList(); 
                Cache::Add($cache_key, $cache_value, $cache_time);
            }
        }
        return $cache_value;
    }
}
加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。假设在高并发下,缓存重建期间key是锁着的,这是过来1000个请求999个都在阻塞的。同样会导致用户等待超时,这是个治标不治本的方法!
注意:加锁排队的解决方式分布式环境的并发问题,有可能还要解决分布式锁的问题;线程还会被阻塞,用户体验很差!因此,在真正的高并发场景下很少使用!

随机值伪代码

//伪代码
public object GetProductListNew() {
    $cacheTime = 30;
    $cacheKey = "product_list";
    //缓存标记
    $cacheSign = cacheKey + "_sign";

    $sign = Cache::Get($cacheSign);
    //获取缓存值
    $cacheValue = Cache::Get($cacheKey);
    if ($sign != null) {
        return $cacheValue; //未过期,直接返回
    } else {
        Cache::Add($cacheSign, "1", $cacheTime);
        ThreadPool.QueueUserWorkItem((arg) -> {
            //这里一般是 sql查询数据
            $cacheValue = GeList(); 
            //日期设缓存时间的2倍,用于脏读
            CacheHelper.Add($cacheKey, $cacheValue, $cacheTime * 2);                 
        });
        return $cacheValue;
    }
} 

(4).Redis实例发生宕机产生缓存雪崩

1. 业务系统中实现服务熔断或者请求限流机制

服务熔断:发生缓存雪崩时,为了防止发生连锁的数据库雪崩,甚至整个系统崩溃,暂停业务应用对缓存系统的接口访问。

具体实现:业务调用缓存接口时,缓存客户端不再请求Redis,而是直接返回,等到Redis缓存实例重新恢复服务以后,在允许应用请求发送到缓存系统。

优点:避免了大量请求因缓存缺失,而积压到数据库系统,保证了数据库系统的正常运行。

2.事先预防

搭建主从节点构建Redis缓存高可用集群

当Redis缓存的主节点故障宕机了,从节点切换为主节点,继续提供缓存服务避免缓存实例宕机造成的缓存雪崩问题

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

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

相关文章

多孔弹性材料中传播的膨胀波方法(Matlab代码实现)

👨‍🎓个人主页:研学社的博客💥💥💞💞欢迎来到本博客❤️❤️💥💥🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密…

海量并发低延时 RTC-CDN 系统架构设计(下)

上半部分内容:海量并发低延时 RTC-CDN 系统架构设计(上)低延时 RTC-CDN 系统的架构传统 CDN 直播发展多年,为了优化延时,业界基本上朝两大优化方向:优化传输层协议和在传输层协议的基础上优化应用层协议。R…

Hadoop环境搭建(1)

一、在已安装的虚拟机上面进行修改(以CentOS 7为例)①修改主机名查看自己主机名,命令hostname第一种修改主机名方法,命令vi /etc/hostname第二种修改主机名方法,命令hostnamectl set -hostname 自定义主机名可以更改为…

简单介绍如何使用robotium进行自动化测试

创建一个简单的工程作为被测程序,运行后显示如下界面: 2.创建一个Test Project 1). 打开eclipse,选择File->New->Project…->Android Test Project,点击Next。 2). 在Test Project Name中输入测试工程的名称&#xff…

华为HCIP-Datacom认证题库(H12-821)

第一套(100题) 1. (单选题)下面关于0SPF的特殊区域,描述错误的是: A.Totally Stub Area允许ABR发布缺省的三类LSA,不接受五类LSA和细化三类LSA B.NSSA Area和Stub区域的不同在于该区域允许自治系统外部路由的引入&…

「实践总结」订单超时自动取消

在进行开发的过程中,在开发的时候,有遇到相关的延时支付相关的问题,在解决延时支付的相关的问题的时候,会有很多种的解决办法,现在就讲对应的解决办法先进行相关的总结操作;「引言」在开发中,往…

mysql索引失效的几种情况

失效的几种情况 1、select * from xxx 2、索引列上有计算 3、索引列上有函数 4、like左边包含‘%’ 5、使用or关键字 6、not in和not exists 7、order by 8、不满足最左匹配原则 给code、age和name这3个字段建好联合索引:idx_code_age_name。 该索引字段的顺…

ChatGPT告诉你:项目管理能干到60岁吗?

早上好,我是老原。这段时间最火的莫过于ChatGPT,从文章创作到论文写作,甚至编程序,简直厉害的不要不要的。本以为过几天热度就自然消退了,结果是愈演愈烈,热度未减……大家也从一开始得玩乐心态&#xff0c…

注意,这本2区SCI期刊最快18天录用,还差一步录用只因犯了这个错

发表案例分享: 2区医学综合类SCI,仅18天录用,录用后28天见刊 2023.02.10 | 见刊 2023.01.13 | Accepted 2023.01.11 | 提交返修稿 2022.12.26 | 提交论文至期刊部系统 录用截图来源:期刊部投稿系统 见刊截图来源&#xff1a…

npm link

正文npm link的用法假如我们想自己开发一个依赖包,以便在多个项目中使用。一种可行的方法,也是npm给我们提供的标准做法,那就是我们独立开发好这个 "依赖包",然后将它直接发布到 npm镜像站 上去,等以后想在其…

熟读阿里总结的 Java10w 字总结,15 天拿下 5 个大厂 offer(阿里,美团,字节...)

Java 面试都会有很多程序员找工作、跳槽等一系列的安排。说实话,面试中 7 分靠能力,3 分靠技能;在刚开始的时候介绍项目都是技能中的重中之重,它也是可以决定一次面试的成败的,那么在面试的时候你会如何介绍自己、熟练…

Sms多平台短信服务商系统~完成阿里云短信服务发送可自行配置

1.项目中引入Maven 阿里云地址 不同编程语言都有对应的SDK,你们下载自己需要的即可。 pom.xml中添加maven坐标 <!--阿里云短信服务--><dependency><groupId>com.aliyun</groupId><artifactId>alibabacloud-dysmsapi20170525</artifactId>…

八、CSS新特性二

文章目录一、CSS3多背景和圆角二、怪异盒子模型三、多列属性四、H5多列布局瀑布流五、CSS3线性渐变5.1 线性渐变5.2 径向渐变六、CSS3过渡动画七、CSS3 2D八、CSS3动画一、CSS3多背景和圆角 css3多背景&#xff0c;表示CSS3中可以添加多个背景。 CSS3圆角 border-radius: 0px;…

日本机载激光雷达测深进展(二)机载激光雷达测深经验

日本海岸警卫队海洋情报部&#xff08;JHOD&#xff09;拥有14年的机载激光雷达测深(ALB)经验。由于ALB调查高效率和高分辨率&#xff0c;JHOD已将ALB应用于各种用途&#xff0c;如制图、海啸受灾港口的恢复重建、安全监测和火山活动研究。本文简要描述了JHOD激光雷达测深系统的…

基于Hibernate对数据库表的单表查询

基于Hibernate对数据库表的单表查询 1.依赖 1.1jar包 1.2配置文件。persistence.xml <?xml version"1.0" encoding"UTF-8"?> <persistence version"2.1"xmlns"http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi"…

docker 部署centos7.9并打包成docker

下载centos基础镜像 docker pull centos:centos7 运行镜像 docker run -itd --name centos-test -p 60001:22 --privileged centos:centos7 /usr/sbin/init 进入容器 docker exec -it ebec90068696 /bin/bash 配置容器信息 安装ssh服务和网络必须软件 yum install net-to…

Linux基础命令-pstree树状显示进程信息

Linux基础命令-uname显示系统内核信息 Linux基础命令-lsof查看进程打开的文件 Linux基础命令-uptime查看系统负载 文章目录 前言 一 命令介绍 二 语法及参数 2.1 使用man查看命令语法 2.2 常用参数 三 参考实例 3.1 以树状图的形式显示所有进程 3.2 以树状图显示进程号…

【计算机网络 -- 期末复习】

例题讲解 IP地址&#xff08;必考知识点&#xff09; 子网掩码 子网划分 第一栗&#xff1a; 子网划分题目的答案一般不唯一&#xff0c;我们主要采用下方的写法&#xff1a; 第二栗&#xff1a; 路由跳转 数据传输 CSMA/CD数据传输 2、比特率与波特率转换 四相位表示&am…

一文高端Android性能优化-总结篇

以下从几个方面来总结一下Android的性能优化&#xff1a;1&#xff1a;界面卡顿优化2&#xff1a;内存优化3&#xff1a;App启动优化界面卡顿优化Android的界面为每秒60帧&#xff0c;即必须在16ms内完成1帧的绘制&#xff0c;如果某个方法耗时过程&#xff0c;导致16ms内无法完…

OIDC OAuth2.0 协议及其授权模式详解|认证协议最佳实践系列【1】

OIDC / OAuth2.0 是一种开放的标准&#xff0c;可以帮助应用程序安全地访问用户的资源&#xff0c;而无需将用户的凭据&#xff08;如用户名和密码&#xff09;暴露给应用程序&#xff0c;我们可以通过标准协议&#xff0c;建立集中的用户目录和统一认证中心&#xff0c;将内外…