你了解 Redis 穿透和雪崩么?
为什么这么说,因为面试官当你说到 Redis 的时候,面试官问的现在已经不是 "你说一下 Redis 的几种数据结构" ,现在面试问的时候,很多都是对 Redis 的实际使用开始问了,比如说,
- Redis 都有哪些架构模式?单机版,主从复制,哨兵机制,集群(proxy 型),集群(直连型)
- 使用过 Redis 分布式锁么,它是怎么实现的?
- 使用过 Redis 做异步队列么,你是怎么用的?有什么缺点?
- 什么是缓存穿透?如何避免?什么是缓存雪崩?何如避免?
而阿粉的小学妹遇到的就是关于 Redis 的缓存穿透和雪崩问题了。这个问题学妹配合了一波自己的 UI 功底图加上口头的解释,于是成功的拿到了这个 Offer,也可能是因为小学妹比较美丽并且技术还过的去。所以,就准备入职了。
我们来看看小学妹到底画了什么图,让面试官问了一波之后就入职了。
缓存穿透
如图:图是阿粉找小学妹专门画出来的,大家看一下
既然我们看完图了,相信大家也都看到了什么是缓存穿透了,也就是说,在我们的缓存系统中,也就是 Redis 中,我们都是拿着我们的 Key 去 Redis 中去寻找 Value 中,如果说我们在 Redis 中找不到我们的数据之后,我们就会去数据库中去寻找我们的数据,如果只是单一请求的话,也不能算是个太大的问题,只能称之为击穿而已,但是如果说要是请求并发量很大的话,就会对我们的数据库造成很大的压力,这其实就称之为缓存穿透,而穿透出现的严重后果,就会是缓存的雪崩了,我们先说穿透,一会再说雪崩。
那么都会有什么情况会造成缓存被穿透呢?
- 自身代码问题
- 一些恶意攻击、爬虫造成大量空的命中。
如果有个黑客对你们公司的项目和数据库比较感兴趣,他就可能会给你整出巨多的一些不存在的ID,然后就疯狂的去调用你们的某项接口,这些本身不存在的 ID 去查询缓存的数据的时候,那就是压根没有的,这时候就会有大量的请求去访问数据库,虽然可能数据能支撑一段时间,但是早晚会让人家给你整的凉了。
那么应该怎么去解决缓存穿透的问题呢?
- 利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试。
- 采用异步更新策略,无论 Key 是否取到值,都直接返回。Value 值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。
- 提供一个能迅速判断请求是否有效的拦截机制,比如,利用布隆过滤器,内部维护一系列合法有效的 Key。迅速判断出,请求所携带的 Key 是否合法有效。如果不合法,则直接返回。
布隆过滤器实际上是一种比较推荐的方式。
布隆过滤器的实现原理则是这样的:
当一个变量被加入集合时,通过 K 个映射函数将这个变量映射成位图中的 K 个点,把它们置为 1。查询某个变量的时候我们只要看看这些点是不是都是 1 就可以大概率知道集合中有没有它了,如果这些点有任何一个 0,则被查询变量一定不在;如果都是 1,则被查询变量很可能在。注意,这里是可能存在,不一定一定存在!这就是布隆过滤器的基本思想。
而当你说出布隆过滤器的时候,可能这才是面试官想要问你的内容,这时候你就得好好的和面试官开始聊聊什么事布隆过滤器了。
我们还是继续用大众都想看到的图解来解释布隆过滤器。
字符串 "Java" 在经过四个映射函数操作后在位图上有四个点被设置成了 1。当我们需要判断 “ziyou” 字符串是否存在的时候只要在一次对字符串进行映射函数的操作,得到四个 1 就说明 “Java” 是可能存在的。
注意语言,是可能存在,而不是一定存在,
那是因为映射函数本身就是散列函数,散列函数是会有碰撞的,意思也就是说会存在一个字符串可能是 “Java1” 经过相同的四个映射函数运算得到的四个点跟 “Java” 可能是一样的,这种情况下我们就说出现了误算。
另外还有可能这四个点位上的 1 是四个不同的变量经过运算后得到的,这也不能证明字符串 “Java” 是一定存在的。
而我们使用布隆过滤器其实就是提供一个能迅速判断请求是否有效的拦截机制,判断出请求所携带的 Key 是否合法有效。如果不合法,则直接返回。
而阿粉的小学妹给面试官解释了一波这操作之后,看样子,面试官对这个“程序猿”开始有点印象了,接下来就顺着问了,那什么事缓存的雪崩呢?
缓存雪崩
这时候也就是说,当我们有多个请求访问缓存的时候,这时候,缓存中的数据是没有的,也就是说缓存同一时间大面积的失效,这个时候又来了一波请求,结果请求都怼到数据库上,从而导致数据库连接异常
他和穿透实际上相似但是又有所不同,相似的地方是都是搞数据库,不同的是缓存穿透是指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库
而解决缓存雪崩的策略也是比较多的,而且都是比较实用的。比如:
- 给缓存的失效时间,加上一个随机值,避免集体失效。
- 双缓存。我们有两个缓存,缓存 A 和缓存 B。缓存 A 的失效时间为 20 分钟,缓存 B 不设失效时间
双缓存策略比较有意思,当请求来临的时候,我们先从 A 缓存中获取,如果 A 缓存有数据,那么直接给他返回,如果 A 中没有数据,那么就直接从 B 中获取数据,直接返回,与此同时,我们启动一个更新的线程,更新 A 缓存和 B 缓存,这就是双缓存的策略。
上述的处理缓存雪崩的情况实际上都是从代码上来进行实现,而我们换个思路考虑呢,也就是从架构的方向去考虑的话,解决方案就是以下的几种了。
- 限流
- 降级
- 熔断