【毁三观系列】缓存雪崩、穿透竟该这么答?| 问题竟会一生二、二生四?| 答案为何如此之多?| 系统这么多地方用了缓存?【全网最权威缓存续集(二)】

假设,大厂面试官抛来了这个问题,你会怎么回答?

你如果真的只是把解决方案简单的说一遍,那肯定就当场 pass 了
错误示范:加锁、分散过期时间、布隆过滤器

当然,我不是说答案是错的,而是说,你不能这么去回答。

其实之前,对于缓存的击穿、雪崩、穿透,其实我已经写过一篇文章来详细阐述了。不过,我主要还是针对 Redis 这样的缓存中间件来谈及的,大部分涉及到的是技术上的难点,并且也是以一个超高并发的角度来思考、理解、以及加以阐释的。
全网最权威!Redis缓存击穿、雪崩、穿透!刷新你的三观!!!

不过,虽然文章显得很牛,不过仔细看来,你会发现,我重点也仅仅是在技术层面。(不过,你要是能把缓存击穿这个一层又一层的问题一个个抛出来然后又一个个解决,还是很不错的哈。)
那么,既然讨论到了缓存问题,涉及到了一个架构的设计思路,所以,还是得综合考虑业务的。

脱离业务的架构,绝对不是一个好的架构!

因为表面上的一个问题,它是一个问题,却又不是一个问题。

这话可能很迷糊:

  • 当你从技术的角度去看待的时候,你会发现这是一个技术问题
  • 当你尝试去解决这个问题,你会发现可能又出现了新的问题
  • 然后当你追究问题的源头,你会发现这可能有多个问题
  • 然后当你在结合不同的业务,你会发现这些问题又变成了不同的问题

WTF !!!
所以当你就真的只是回答了这一个点,答了一句话,那可是要回去登通知的。
在这里插入图片描述

所以,上一篇文章,就可以看成,是把一个技术点,去疯狂挖深
而对于缓存这块的知识,我还是决定,再结合业务,以及更多地层面,去探讨探讨其中的问题,以及解决的一些思路。
那么,你在和面试官谈的时候,就要不慌不忙,从一点一点的场景,去娓娓道来,积小流成江海;
那面试官就会觉得,你是对这些问题有想法的人了。
在这里插入图片描述

缓存所在之处

首先,提及缓存,有些人学习了 Redis 之后,就会本能地自动将 Redis 和缓存这两者绑定起来,认为缓存就是 Redis,Redis 就是缓存,从而忽略了其他缓存的存在。

而实际上,我们谈及一个系统、或者说一个架构的缓存,那么,不仅仅是 Redis 这类的缓存中间件,一个系统中,涉及到缓存的地方,数不胜数。

比如:

  • 最开始,用户的浏览器访问的时候,最前端,就是浏览器的本地缓存;
  • 不仅如此,一个项目,往往会把静态资源,和用户的动态请求区分开来,将静态资源,分发到 CDN上。这样,在 CDN 这里,又做了一层缓存;
  • 即使请求达到后端,在 Nginx 这里,一般,我们会本能联想到动静分离、反向代理等技术。不过,Nginx 同样可以做缓存,这又是一层缓存;
  • 在往后,到了业务处理集群,我们往往才会想到我们最熟悉的缓存中间件,Redis、Memcached 等等;
  • 不过,不要忽视了,很多时候,由于 Redis 这样的缓存中间件还是设计网络 I/O 开销,所以,速度就不及本地缓存。所以,后端业务系统,也同样可能有本地缓存;
  • 不仅如此,即使到了数据库,在数据库服务器也可能有缓存。

所以,谈到缓存,不仅仅只是 Redis 的问题,其中还涉及到很多很多方面,因为一个系统中有缓存的地方,是十分多的。

缓存所带来的问题

那么,既然有那么多的缓存,就会涉及到和缓存有关的众多问题。

比如:

  • 缓存的击穿、雪崩、穿透;
  • 缓存的失效、容量、性能;
  • 缓存的一致性;
  • 缓存的分布式管理、集群模式、sharding 分片;
    等等等等……

所以,缓存的存在问题,是很多的。
很多时候,我们会把缓存的击穿、雪崩、穿透,分开来单独讨论,这样虽然没有什么大问题,不过,对于缓存来说,其实很多问题都是共通的。
所以很多东西都是表面,我们要探求的,就是它的本质

我们去构建一个高并发系统的时候,难点在于,高并发不是由于某一个环节,而是由所有环节环环相扣的。一旦某个环节出了问题,那么系统就会出现问题。

而我们进行学习的时候,无论如何,都得记得 3W1H:
也就是 When / Where、What、Why、How。

而系统运行的时候,也是难免会出问题的,我们不可能说最初不管问题,碰到了问题再去解决。所以我们都往往会先假设,可能会出现什么问题。
我们假设会出现什么问题,那么生产中就很有可能出现什么问题(神仙莫非定律…)。所以我们可以先尝试去解决这些问题,这样,就可以尽量避免在生产环境出现问题。

所以,对于一个最常见的缓存使用场景,我们先去 Redis 中查询数据,如果能查找到,就返回结果;否则,就前往数据库查询。

那么,我们现在就需要去假想,出现什么样的情况,会使得访问缓存查询不到数据?

  • 比如首先很常见的,网络不通了;
  • Redis 自身也会出问题,比如说 Redis 挂了;
  • 也可能是缓存过期了
  • 或者 Redis 重启了;
  • 也可能是内存不够用,淘汰掉了部分数据。
  • 数据不存在

所以,我们查询不到缓存,其实不单单是缓存过期,它会有很多种可能性。
那么,我们就可以针对我们想到的这些可能出现的异常情况,来思考如何去处理这样的场景。

其实,我们的程序、架构是怎么来的,其实就是我们不断地根据底层的原理,基于一定的技术和了解,去提出各种各样的假设:
它会出现什么样的结果,它为什么会这样子,我为什么要去用它,如果现在出问题了怎么办,会出现什么样的问题……
通过不断地去问问题,去横看我们的架构,这样才能把我们的架构,打磨地越来越完美,不断地接近完美。
我们并没有天生就了解这个系统会出现什么问题,也无法预知,以后会出现什么问题,我们遇到的场景可能也是前人没有碰见过的。
但是,我们都能了解,这么一个问题的本质,它是不会变的。

就比如我们这里的 Redis,本身就是存在内存里的,如果无法读取缓存,那么问题就会是这么几个。

找出了问题之后,我们就能预料出后果。如果缓存读取不成功,就会导致大量的请求去访问数据库。
而数据库由于要磁盘操作,所以一定是慢于我们的内存的。
所以,之前我们 Redis 每秒这么多请求,没有什么问题,而给数据库之后,就会给它很大的压力。
所以,数据库就可能会崩溃;那么,相对应的以来于这个数据库的功能,也就崩溃,也就导致了整个系统崩溃;如果还有其他系统依赖这个系统,又会导致其它系统无法提供正常服务。

所以,一连串的,系统开始崩溃,也就会产生我们常说的雪崩。

有句话是:雪崩之下,没有一片雪花是无辜的。

在这里插入图片描述

那么,既然我们找出了可能导致雪崩的原因,也知道可能会导致的后果。
那么,我们就需要去想,这些问题,能不能去解决,或者说,怎么样能去避免。
有些问题不一定能去解决,那么我们也不会一定要去 100% 追求完美的状态,而是保持一个恰恰好的状态,针对于我们自己的业务,将其保持一个恰好的状态,问题存在,但也不会对系统造成过大的影响。

那么,下面看一下,对于这些情况,分别如何处理这些问题:

处理问题

首先,可能是网络不通的情况。
如果出现缓存未命中的这种情况,那么,我们就有必要去关注一下错误日志,比如我们的网络不通这种情况,虽然说不是业务系统要去解决的,但是很多时候也不得不防。
因为生产环境这样的情况,是任何稀奇古怪的事情都可能发生的,比如什么网线被你一踩断了;网线被老鼠咬断了等等……或者,如果是这种发展非常迅速的公司,运维人员常常会跟不上公司发展速度,那么,服务器就很可能出各种各样的问题。
在这里插入图片描述
所以通常情况下,我们都会直接抛异常,记录错误日志。

第二种情况,缓存失效了,那可能是 Redis 挂了
不过这种情况一般不用太担心,因为我们一般配置集群的时候,都会配置一个主从、或者主备的集群,或者是 cluster、还有哨兵这样高可用的集群。
所以一般 Redis 集群可用性还是很强的,就算有节点宕机,也不至于使 Redis 缓存不可用。

第三种情况,就是我们常说的缓存过期
很多时候,我们探讨的也就是缓存过期,引发的雪崩、击穿、穿透,这样问题的技术解决方案。
不过,我们实际上是不能假设有一套方案是完美的。
因为,这类的缓存失效带来的问题,是针对于业务系统的。每一个公司,都有自己不同的业务系统。所以,这也就是产生争议的地方。

有人说,我们缓存不过期的呀!
比如说,像我们的系统配置,也就是那一些基本上不会变的、并且数据量小的,就可以设置不过期。
不过,这种不过期的终究只是少部分是吧,并且即使让它过期,也不会产生巨大的不利影响是吧。

所以,肯定还是得有数据过期
那么,就会有冷热数据分离

冷数据,就是指那部分访问频率不高的;热数据,就是那部分访问频率很高的数据。

比如我们的用户数据,用户每天都会用到的,就可能有这么一种情况:
有的用户,可能每天都会登录;而有的用户,则可能注册使用过一次之后,之后就再也没有使用过该业务系统。
所以,我们可以对我们的热数据,采用定期更新的方式;
而对于冷数据,则采用一定的过期时间。
也就是我们不会把所有的数据,都丢进我们的内存里面的。
你想想看,我们数据库表里有那么多数据,要是全丢给内存,内存需要多大。
我们给机器用这么大的内存,那么,这些内存,都是花昂贵的价钱买来的。
所以,我们去看这个问题的时候,不能看成是一个纯技术问题,这涉及到成本,要有成本思维。
假设,对于一个没啥用户的应用,那么很可能几个 G 内存就够用了;
如果,是对于一个高并发的应用来说,那么,就必须设置一个有效期,根据自己的业务系统,来设计过期时间是多少。

假设,如果是一个电商平台,那我们的用户信息过期时间关系不会特别大,因为电商平台,用户操作并不会那么集中
如果不集中的话,可能日活一千万,可能分到每一秒,也就一百的并发而已,所以并不是很高。
只不过,用户的访问,不会那么平均,可能在早上起来上班的时候,晚上下班回家的时候,或者睡觉前,刷一刷手机,访问的用户就会比较多,而其它时间,则会更少。
所以,对于非集中的场景来说,缓存过期时间要求并会特别高。
不过,我们通常也需要对缓存的过期时间,做一个随机的波动。就是不要让大量的数据,在同一时间点同时过期,而是用一个随机的值,来让过期的时间分散开来,不至于太容易发生雪崩。
所以,我们可以发现,对于这类非集中的业务系统来说,设计并不会太麻烦。

但是,对于直播这种场景,就会比较复杂。
因为直播的场景,比较集中
比如,一些知名人士要来直播间,那么肯定都会提前发出预告,比如 xxx 将在 x 月 x 日晚 xx 点开始直播。
那这种情况下,提前预告了什么时候直播,那么,就不只是简单地设计一个过期时间。像这类知名主播,总要来直播间直播的,那么就不用过期了,让他常驻这个系统就可以了。
所以,我们在设计的时候,还会结合到一定的数据分析,针对于一些热数据、冷数据进行分离。
在后台,就可以给这种大 v 打上一个标签,那么,可能有人直播要去看他的信息,就算不直播,也有人要去看一下他的信息,总之就是无时无刻都访问量比较大。
那么,就可以设置为不过期,或者过期时间设置得长一些,一个月,或者两三个月。

第四种情况,内存不够用
很可能就是我们前期规划得就不好,内存没有分配充足。
而一般系统崩溃的原因,往往不是前面几点,而是这一点。
通常情况就是,一般互联网企业,在一开始的时候,就没想到用户量会这么大,可能一开始,大家只是琢磨着,搞了个普通的系统,可推广起来之后,用户量超出了预期。
所以,这时候即使用了缓存,那么由于前期规划不足,很多信息都需要缓存,那么,在内存管理里面,通常就会被 LRU、LFU 淘汰。
一般来说,被淘汰的数据,都是没什么人用的数据,冷门数据。但是,现如今的情况下,这种机制还是会有点问题。
因为,如今的时代,可以叫网红时代,什么都可以一夜爆红。
所以,这些东西,我们就无法预知。
前面的问题,我们可以通过一些手段来解决,但是对于这样的情况,我们就需要扩大内存。

第五种情况,Redis 重启了
因为数据是存在内存里的,那么重启了,自然也就没了。
那么可以持久化,不过持久化,也不能保证数据完整地落到磁盘。
所以说,缓存失效,是一个避不开的问题。

不过,还有一种情况,导致缓存失效,是因为压根就不存在这么一条数据。
这个问题,在业界有一个专门的说法,叫做缓存穿透。
而这个问题,也是不能够去避免的。

缓存穿透

在缓存失效的最后一点里,数据本身就不存在,也就是所谓的缓存穿透
这就不是我们的系统问题,因为我们的系统没问题,缓存也好好的,数据库也很正常。
但是用户就是查那些本来就不存在的那些数据,比如说我们公司的竞争对手
这种事情很常见,尤其是一些初创型的企业,再常见不过了,连美团支付宝,线下也都要打架。
所以,如果有人去查询这样必然不存在的数据,而你的系统,恰好就没有对应的机制,那么,这时候,你的系统,可能就被攻击透了。
而且,这些请求,又恰好都是正常的请求,也没有什么不对的地方,就算发生了,你也可能感觉不出来,你可能就是发现,这个系统挂了。
在这里插入图片描述

所以,我们往往要对重要的接口,进行查询校验
所以,参数不是乱来的,这一块就已经算是涉及到了防范攻击了。
怎么防止他乱填,那就要参数校验,比如,搞一些特殊的生成方式。
可能用户看到的 id,就是一串 1234567 的数字,但是这个数字,可能就不是随便写的,它是会被赋予一种方式,计算得出的。
比如说,id 的第一位必须是奇数;比如说,第三位数字,必须是 3……等等等等
也就是,我们的 id 可以是一串数字,但是,它不是一个纯粹的从 1 开始递增的一个数字,而是有特殊意义的数字,我们可以不通过查数据库查缓存发现它不存在,而是只通过一个简单地校验,就把它校验出来。而这个规则,一般人也很难去猜到。
不过,这么做有效,单也不一定能全部防范。
不管怎么说,这些东西,都是根据你的业务规则,去进行调整的。

第二种方式,就比较直接。
我如果在数据库里也查询不到,那么,我就直接在缓存里放入一个值表示该记录不存在。
这样的话,我们就可以把有效期设置得比较短。
因为,这个数据虽然现在不存在,但是不代表未来也不存在,因为能被查询,说明那肯定是符合我们的参数校验规则的。
所以,这种方案,就是临时缓存表示空值

如果,这些问题都解决不了。
那么,就需要在整个系统中,校验一下,整个系统中是否存在这个数据。
简单地做法,就是把所有的 id,存在 Redis 里面,每次查询时,先看看 Redis 中有没有这个 id,有的话,就代表数据存在。
这个做法对很多数据量不是很大的系统来说,也是完全 OK 的。
不过,有一个问题就是,对于那些量很大的应用系统,把 id 全部放入内存是不现实的。
假设,一个 id 用 long 来表示,那么一个 id 就是 8Byte,那么10E 的商品,就是大约 8G 的内存空间。
而且,除了商品 id,我们系统肯定还会有用户 id,订单 id 等等……
所以,光是存这些 id,我们就会耗费大量的内存,资源的消耗就过于多了。
不过对于一些小系统,少掉几个数量级,变成 M、K 的数量级,倒也是很合适的。所以,我们往往也不用去死记硬背,或是生搬硬套,非要整出一个布隆过滤器上去。

只有碰到真正这样的海量数据,我们就得思考,怎么去解决。
在业界就有一个算法,既然要减少内存占用,那就可以用布隆过滤器,它来自于 1970 年,布隆提出来。
它就有点类似 HashMap 这样的做法,根据 key 取一个 hash,然后,定位一个 index,也就是下标,然后再判断,这个下标有没有数据,没有,就代表空。
布隆过滤器就类似如此,也是用一个数组,只不过,这个数组,不存放 id,因为这个 id 太占空间了。
它是一个 bit 数组,每一个下标,对应一个 bit,只能存 0 或者 1。
所以,这个数组很长,但是,占用的空间,却又非常非常小。

不过,布隆过滤器虽然看起来很优秀,非常节省空间,
但是,布隆过滤器是由缺点的:
因为哈希碰撞是无法避免的,所以,就存在那些不存在的 id,它们对应的数组下标对应到一个存在 id 的下标,那么就会使得博隆过滤器误判该数据存在。

所以,布隆过滤器,为了尽力减少冲突,布隆过滤器一般采取多重哈希,只有所有哈希值都满足,才判定为存在。
在这里插入图片描述
不过,其实有些时候,这些也完全都是多余的:
如果,你的 id 都是自增 id 的话,那么,是不是只要判断一下,有没有超过最大 id 就好了?
没有超过最大 id 的,就是正常请求,超过最大 id 的,就是非法请求。

所以,实际上,采取什么措施,都是根据业务系统去进行考量和取舍的。

不过互联网企业正常不采用自增 id,不然的话,很容易就给竞争对手猜出业务量。
在内部使用的话,就可以考虑用自增 id 了。

限流

那么,我们通过上面的方式,发现,有了各种各样不同的方案,去应对我们的缓存失效,来保证服务的良好运行;
但是,终究无法做到 100%
也就是一定会有漏网之鱼
在这里插入图片描述
所以说,缓存失效,我们可以有效控制,但做不到万无一失
比如网络的不稳定,数据的过期,LRU 清除,以及节点的宕机,重启,布隆过滤器的误判……等等等等

那么,首先,不管你遇到的场景是什么,首先,只要你是高并发场景,那么就要记住这四个字:
限流、分流

只要出现缓存雪崩,那么最大的出问题的地方,就是我们的数据库。
所以,我们的问题就可以转化一下:
数据库出问题了,我们该怎么办?
或者说如果要让数据库不出问题,我们该怎么办?

既然是因为请求过多导致数据库扛不住,那么,我们就要不让数据库,接受那么多的请求。
所以,我们可以做的,就是限流
而限流的话,只是个概念,我们究竟该怎么去做?我们一般都有很多小的措施。
典型的限流方法,就是信号量限流
在 Java 的并发包里,就有 Semaphore 这个并发工具类。
每次访问数据库,都要获取这个信号量,比如我们设置 100,那么前 100 个请求还在访问数据库时,第 101 个请求就会阻塞,就会因为获取不到信号量而暂时无法访问数据库,这样,就对数据库进行了一个限流。

不过,有一个很有意思的问题,为什么要在数据库操作之前加一个 Semaphore?
你可能要问,我不是刚刚说了,要限流吗?
不知道你们有没有想过这样一个问题,我们假设,缓存失效的情况下,所有请求,会打到我们的数据库。
不过,所有的请求,会打到数据库吗?
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
在这里插入图片描述

你不要被这个轻易地迷惑了。
我们数据库这块,是不是有一个数据库连接池?
所以,到了数据库连接池这里,大量的请求,连数据库连接都获取不到,怎么去怼我们的数据库?
那么,既然有数据库连接池,那么还为什么要信号量?压根不会出现数据库被怼炸的情况啊?

所以,实际上,用信号量,实际是一个比较偷懒的方法。
在很多用户同时查数据的时候,可能一堆用户查询的是主播 A,一堆用户查询的是 B,还有一堆查询 C,还有 D。
那么,就可不可能有这么一种情况,Semaphore 信号量,被查询 A 的用户全部抢走,那么剩下的 B、C、D 的用户是不是就不爽,也就是其它主播的粉丝很不爽。
所以这种情况下是不太友好的。
那么我们可以针对不同的主播,分别设置一个 Semaphore 信号量,那么,在用户访问的时候,不同主播的粉丝,就不会出现等待差异过大的问题了。

那么,再回到这么一个问题,为什么在一个系统里有数据库连接池还需要 Semaphore。
因为,一个系统中的数据库连接池是给系统中的整个应用使用的,而比如查询主播,可能只不过是众多接口中的一个罢了。
那么,假设我们不加任何限制,完全利用数据库连接池的机制来限流。那么,假设,这一个查询主播的功能,就用光了连接池里的所有连接,那么,其它功能还玩啥?
所以说,再加一个 Semaphore 信号量,是为了再做一个隔离
而这一个功能,只允许用一部分的资源,剩下的资源,还需要留给其它功能去使用。

我们经常和限流划在一起的,是降级

虽然说,限流可以抑制住大量请求,但是,Semaphore 信号量会使得大量请求阻塞等待。用户那里,就会一直转圈圈,卡在那访问不到,但是又还在停在那等着出结果。所以用户体验就会特别差。
比如双十一的时候,系统用不了会怎么样,是不是会显示:失败请重试;查询状态出现错误;网络不给力……等等反馈。
不过,降级的话,是随随便便就可以降得吗?
尤其是支付宝、微信等等应用,你能看到的降级页面,这些语句、词语,都是通过重重审核下来的。
要是支付宝天天都给你弹出错误,那这个也说不过去了。
也就是系统得有一定的容错机制,比如重试一下,而不是一碰到问题就给错误,就降级。
所以,降级常常也会和一个词连在一起,就是容错,就叫做:
容错降级

作者的碎碎念

其实大家很多时候,都只是把缓存雪崩、穿透之类的,单独看成一个问题,意图是找到一个完美的方法。
而我这里提到的,也不仅仅只是一个缓存的过期,而是一系列缓存可能失效的原因,这些往往是大家会疏忽的地方。
很少有人说,可以把这些问题联系起来,把问题综合起来,能结合不同的场景去看待的。
而现实中也往往没有一个完美的方法,能完整地解决这样的问题,因为在不同的场景中,问题往往又会有不同的变化。
所以,要想成为一名优秀的架构师,那思维就一定不能有局限,你能想到别人想不到的问题,那你才能比别人多一份机会。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值