老司机带你玩转面试(2):Redis 过期策略以及缓存雪崩、击穿、穿透
前文回顾
建议前一篇文章没看过的同学先看下前面的文章:
「老司机带你玩转面试(1):缓存中间件 Redis 基础知识以及数据持久化」
过期策略
Redis 的过期策略都有哪些?
在聊这个问题之前,一定要明确的一件事情是,如非必要,任何进入缓存的数据都应该设置过期时间,因为内存的大小是有限的,一台机器可能就那么几十个 G ,你不能拿内存和硬盘比,一台机器硬盘几个 T 都是洒洒水,只要想装,几十个 T 都装得下,关键还不贵。
Redis 设置删除策略,主要有两种思路,一种是定期删除,另一种是惰性删除。
定期删除:
设定一个时间,在 Redis 中默认是每隔 100ms 就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除。
注意这里是随机抽取一些设置了过期时间的 key ,而是扫描所有,试想这样一个场景,如果我有 100w 个设置了过期时间的 key ,如果每次都全部扫描一遍,基本上 Redis 就死了, CPU 的负载会非常的高,全部都消耗在了检查过期 key 上面。
惰性删除:
惰性删除的意思就是当 key 过期后,不做删除动作,等到下次使用的时候,发现 key 已经过期,这时不在返回这个 key 对应的 value ,直接将这个 key 删除掉。
这种方式有一个致命的弱点,就是会有很多过期的 key-value 明明已经到了过期时间,缺还在内存中占着使用空间,大大降低了内存使用效率。
所以 Redis 的过期策略是:定期删除 + 惰性删除。
实际上简单的定期删除 + 惰性删除还是会存在问题,定期删除可能会导致很多过期 key 到了时间并没有被删除掉,然后我们也没有及时的去做检查,也没有做惰性删除,此时的结果就是大量过期 key 堆积在内存里,导致 Redis 的内存被耗尽。
咋整?答案是走内存淘汰机制。
内存淘汰机制都有哪些?
Redis 内存淘汰机制有以下几个:
- noeviction: 当内存不足以容纳新写入数据时,新写入操作会报错。这个一般没啥人用吧,太傻了。
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。
- allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 key。这个一般没人用吧,为啥要随机,肯定是把最近最少使用的 key 给干掉啊。
- volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 key(这个一般不太合适)。
- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key。这个一般也没人用吧。
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除。
接下来,面试官就有可能要求手写一个 LRU 算法了,从头开始写确实有点困难,那代码量太大了,现场写也不大现实,但是借助 JDK 的现有的数据结构,写一个 LRU ,还是应该掌握下的,篇幅原因,我就不贴代码了,这一段代码一搜一大把,大家自己百度吧。
缓存雪崩、击穿、穿透
这几个问题也是在面试中经常会问到的,因为在实际的使用过程中,这一部分内容往往牵动了整个系统的安全性以及稳定性。
缓存雪崩
先了解下什么是缓存雪崩?
我先描述两个使用场景:
场景一:
线上正在运行的系统,突发状况, Redis 挂掉了,导致大量的数据访问不走 Redis ,直接落在了 DB 上,DB 扛不住这么大量的访问,直接崩掉了,这时如果 BD 是独立使用还好,如果不是独立的, DB 还和其他业务功能共享,那么势必会导致其他使用同一 DB 的业务功能一起崩掉,又会导致依赖于这些业务功能的其他系统接着完蛋,结果就是所有的系统和功能一起上天,这时,可能祭天一个程序员就不够了,整个 IT 部都要一起上天了。
场景二:
比如说电商平台,首页的热点数据都是放在缓存中的,早晨 8 点刷新了一批热点数据,设置有效期是 4 个小时,而在这 4 个小时中,并没有刷新新的热点数据,恰巧在中午 12 点的时候有一个秒杀活动。
悲惨的结果已经可以预见了,当时间走到 12 点,热点数据集体失效,而秒杀活动导致大量的用户疯狂的涌入,热点数据不存在,请求的数据直接落到了 DB 上,导致 DB 崩掉,重复一遍和场景一一样的惨况,整个 IT 部被拿来送上太阳,和我们的特朗普老师肩并肩。
上面两个场景的结果都是一样的,因为 Redis 的功能不可用,导致 BD 上天,从而导致 IT 部集体飞升。最坑的是这种事情发生后,还没有一个简单易行的处理方案,因为单纯的重启恢复服务这个套路已经不适用了,流量大的情况下,服务是起不来的好哇,服务刚起来就被流量干爬下,这种事情在国内某知名互联网公司发生过。只能是先在网关把所有流量拦截掉,然后恢复后端服务,并同步补充 Redis 的热 key ,等这些事情都 ok 以后,才能将流量放进来重新恢复服务,这么一套搞下来,几个小时没有了吧,当然,这个耗时视公司的不同而不同。
那么这种情况应该如何处理,先说说场景二,首先是不能使得所有的热点 key 同时失效,这里简单加一个随机数就好了,尤其是电商首页这种场景,直接设置热 key 永不失效都是可以的,要更新数据就直接更新,唯一的好处就是保险。
那么场景一这种情况我们还有什么解决方案呢?
首先,第一步要做到的就是 Redis 的高可用,随便什么方案,但绝对不能是单机,集群怎么也比单机挂掉的概率要小得多。
接下来,就是程序要启用本地 ehcache 缓存,还要加上服务限流与降级,服务限流降级前 Netflix 提供的 hystrix 后有 Alibaba 提供的 Sentinel ,选用什么看自己的使用场景,有了这个,至少保证了 BD 不会被一下打死,至少保证了服务的部分可用,哪怕只能保证 20% 的可用,对于用户来讲,就是多刷新几次页面的事情。
最后就是一定要开启 Redis 的持久化, Redis 一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
缓存穿透
什么是缓存穿透?
还是讲一个场景,比如我的数据 id 是从 0 开始的自增序列,普通人请求没啥问题,每次都是用一个正常的 id 来进行访问,但是总有刁民搞事情,每次访问都使用 id 为 -1 来进行访问,因为 -1 并不存在,所有在 Redis 中也不会进行缓存,每次查询 DB 也查不到结果,这种请求直接无视了缓存的存在,直接作用于 DB ,只要访问量超过一定的数量,就会直接把数据库打死。
这种场景的解决方案其实很简单,比如我们可以在程序中对数据的合法性进行校验,如果数据不合法直接返回,比如上面的例子,如果 id 小于 0 ,直接返回失败。
但是单纯的这么操作并不能解决所有的问题,有时候我们并不好判断数据的合法性,就比如上面那个场景,比如数据中 id 最大只有 500 ,但是某些刁民老用一些大值进行请求,比如说 1000 ,这时虽然数据合法,但还是会落在 DB 上,这里我再提供一个简单粗暴的方式,当发现数据没查到数据的时候,在缓存中对这个 key 设置空的 value ,下次再进来就会去走缓存而不会落到 BD 上,这样也能有效防止缓存穿透。
缓存击穿
缓存击穿和雪崩是有点像的,雪崩是大批量的热 key 集体失效或者是不可用,导致请求直接落在 DB 上而打崩 DB ,最终导致整个系统的瘫痪。
而缓存击穿是指在某些情况下,会有一个极热极热的 key ,在扛着非常非常大的并发,突然过期失效,导致所有的请求在一瞬间落在了 DB 上,瞬间击垮 DB 导致全局崩盘。
看到这不知道大家想到了谁,反正我想到的是那个号称七星轨的软件,当然哈,人家的问题并不是缓存击穿,是真的扛不住,缓存直接被打崩了,毕竟我国的全民吃瓜,这个流量还是相当猛的。
都说到这了顺便聊聊它们的情况,第一次是不是缓存击穿现在已经无从考证了,基于这么多次的崩溃看下来,它们家的问题并不是软件层面的问题了,是硬件直接就不够,貌似每次解决问题都是直接联系阿里云扩容,阿里云容量一扩上去问题立马就没了。
你如果非要问硬件为什么不够,很简单么,那么多硬件不要钱啊,带宽不要钱的啊,你给啊,现在硬件带宽开那么高,又不是天天都有明星出轨我们都有瓜吃,空出来的浪费啊。
现在这种操作就挺好,平时硬件够用就行,明星出轨临时扩容,事件热点降低后再缩容回去,最近几次我看到扩容的速度明显快很多了,好几次从我知道崩了不能用到服务恢复只有不到半小时(也有可能是我消息闭塞),感觉阿里云的团队和某博的团队在经历了这么多大瓜以后已经能配合的非常默契了。
扯回来,我们接着说缓存击穿怎么处理。
不同场景下的解决方式如下:
- 若缓存的数据是基本不会发生更新的,则可尝试将该热点数据设置为永不过期。
- 若缓存的数据更新不频繁,且缓存刷新的整个流程耗时较少的情况下,则可以采用基于 Redis、zookeeper 等分布式中间件的分布式互斥锁,或者本地互斥锁以保证仅少量的请求能请求数据库并重新构建缓存,其余线程则在锁释放后能访问到新缓存。
- 若缓存的数据更新频繁或者在缓存刷新的流程耗时较长的情况下,可以利用定时线程在缓存过期前主动地重新构建缓存或者延后缓存的过期时间,以保证所有的请求能一直访问到对应的缓存。