他说他当时,一时间竟然找不到回答问题的角度,感觉自己没有回答到点子上。
我仔细想了一下,确实是感到这个问题有一丝丝的奇怪,有一种让人千言万语,又突然懵逼不知从何说起的神奇力量。
为什么这么说呢?
我们先读题啊,仔细的读一遍题,我给你翻译一下。
如果线上 Redis 挂了。然后所有请求打到数据库导致数据库也挂了。
这是啥?
Redis 挂了,不就是缓存都没了吗?
缓存都没了,不就是缓存雪崩了吗?
缓存雪崩了,不就导致数据库挂了吗?
一提到“缓存雪崩”这四个字,缓存穿透、缓存击穿这几兄弟,是不是就立马条件反射的出现在你的脑海里面了,还顺带着对应的几套解决方案。
然后就像背书似的,什么缓存全没了,什么缓存没有数据库中有,什么缓存和数据库中都没有...
张口就是几分钟不带停顿的。
另外关于缓存击穿和缓存穿透,很多同学都会搞混。
你换一个记法,缓存戳穿、缓存戳透。
穿,只是穿过了缓存。
透,是直接干到底。
你细品,应该就不会记混。
除了上面的“Redis 缓存三连击”这一套八股文之外,还隐藏着另外一个八股文:
Redis 挂了,为什么挂了?怎么就挂了?是不是有单点问题?
这不就是问你 Redis 服务的高可用吗?
说到 Redis 的高可用,脑子里面必须马上蹦出来主从、哨兵和集群吧?
想到这些了,张口又是几分钟不带停顿的。
但是这几分钟的千言万语,马上就被下面这个问题给干懵逼了?
这时该怎么进行恢复?
现在问你怎么恢复,就是事中的事儿了。
你得先说怎么恢复,再说怎么预防。
你要是上来就回答前面说的什么“缓存三连击”、“高可用架构”,还包括大多数同学能想到的多级缓存、限流措施、服务降级、熔断机制,这些都有点牵强。
因为毕竟这些手段都是事前的预防措施,上来就说这些背书痕迹比较明显。
答肯定是要答的,从事中恢复过度到事前预防方案,而且重点就是事前预防,那么我们怎么过度的自然一点呢?
先说事中怎么恢复,其实我觉得几句话就说完了。
服务挂了啊,老哥,还能怎么恢复,当然是重启服务啊。
站在运维人员的角度,当然优先考虑是先把 Redis 和数据库服务重新启动起来啦。
但是启动之前得先做个小操作,把流量摘掉,可以先把流量拦截在入口的地方,比如简单粗暴的通过 Nginx 的配置把请求都转到一个精心设计的错误页面,就是说这么一个意思。
这样做的目的是为了防止流量过大,直接把新启动的服务,启动一个打挂一个的情况出现。
要是启动起来又扛不住了,请在心里默念分布式系统三大利器:
不行就加钱,堆机器嘛。
要觉得堆机器没啥技术含量,你就再从缓存预热的角度答一个。
就是当 Redis 服务重新启动后,通过程序先放点已知的热点 key 进去后,系统再对外提供服务,防止缓存击穿的场景。
而且上面这一系列操作其实和开发人员的关系不大,主要是运维同学干的事儿。
开发同学最多就是在设计服务的时候做到服务无状态,以达到快速水平扩容的目的。
至于怎么去快速水平扩容,那是运维同学的事儿,暂时不要去抢别人的饭碗。
答到这,你就可以用“但是”来过度到事前预防,开始自己的表演了。
故作沉思的对面试官说“but”了:
我觉得从技术方案的角度来说,我们应该做到事前预防。
这一切的问题都是因为 Redis 崩了,也就是发生了缓存雪崩。
在高并发的情况下,除了缓存雪崩,我们还必须得考虑到缓存的击穿、穿透问题。
而且 Redis 为什么会崩了?是不是使用姿势不对?是不是没有保证高可用?
服务中是不是需要考虑限流或者熔断机制,最大程度的保护程序的运行?
或者我们是否应该建立多级缓存的机制,防止 Redis 挂掉之后,大批流量直接打到 MySQL 服务导致数据库的崩盘?
至此,“but”完成,答题的方向从事中恢复,转向了事前预防,进入了我们的强项,八股文专场,然后就可以开始“背诵”了。
我这里简单的聊一下缓存问题三连击和 Redis 的高可用。
至于多级缓存,可以看看我之前发的这篇文章:《这波舒服了,落地多级缓存!》。
缓存击穿
先说一下缓存击穿的概念。
缓存击穿是指一个请求要访问的数据,缓存中没有,但数据库中有的情况。
这种情况一般来说就是缓存过期了。
但是这时由于并发访问这个缓存的用户特别多,这是一个热点 key,这么多用户的请求同时过来,在缓存里面没有取到数据,所以又同时去访问数据库取数据,引起数据库流量激增,压力瞬间增大,直接崩溃给你看。
所以一个数据有缓存,每次请求都从缓存中快速的返回了数据,但是某个时间点缓存失效了,某个请求在缓存中没有请求到数据,这时候我们就说这个请求就"击穿"了缓存。
针对这个场景,对应的解决方案一般来说有三种。
- 第一个就是只放行一个请求到数据库,然后做构建缓存的操作。
多个请求只放行一个,怎么做?
就借助 Redis setNX 命令设置一个标志位就行。设置成功的放行,设置失败的就轮询等待。
- 第二个解决方案就是后台续命。
这个方案的思想就是,后台开一个定时任务,专门主动更新即将过期的数据。
比如程序中设置 why 这个热点 key 的时候,同时设置了过期时间为 10 分钟,那后台程序在第 8 分钟的时候,会去数据库查询数据并重新放到缓存中,同时再次设置缓存为 10 分钟。
怎么样,是不是有点 Redisson 分布式锁看门狗的味道?
我觉得思想是一脉相承的。
只是方案落地的时候,从代码编写的角度来说稍微麻烦了一点。
我曾经也借助这个思想开发过一个流水号系统。
大概是这样的。
流水号系统,属于非常关键的系统,为了降低数据库异常对服务带来的冲击,所以服务启动后会就会为每种业务系统都预先在缓存中缓存 5000 个流水号。
然后后台 Job 定时检查缓存中还剩下多少流水号,如果小于 1000 个,则再预先生成新的流水号,补充到缓存中,让缓存中的流水号再次回到 5000 个。
这样做的好处就是数据库异常后,我至少保证还有 5000 个缓存可以保证上游业务,我有一定的时间去恢复数据库。
这也算是一种后台续命的思想。
- 第三个方法就简单了:永不过期。
缓存为什么会被击穿,是不是因为设置了超时时间,然后被回收了?
那我不设置超时时间不就行了?
如果结合实际场景你用脚趾头都能想到这个 key 一定会是个热点 key,会有大量的请求来访问这个数据。而且这个 key 对应的 value 不会发生变化。
对于这样的数据你还设置过期时间干什么?
直接放进去,永不过期。
其实上面的后台续命思想的最终体现是也是永不过期。
只是后台续命的思想,会主动更新缓存,适用于缓存会变的场景。会出现缓存不一致的情况,取决于你的业务场景能接受多长时间的缓存不一致。
总之,具体情况,具体分析。
但是思路要清晰,最终方案都是常规方案的组合或者变种。