什么是缓存雪崩、缓存穿透、缓存击穿

712 阅读9分钟

缓存雪崩

对于系统 A,假设每天高峰期每秒 5000 个请求,本来缓存在高峰期可以扛住每秒 4000 个请求,但是缓存机器意外发生了全盘宕机,或者这些请求的key刚好都过期了。缓存挂了,此时 1 秒 5000 个请求全部落数据库,数据库必然扛不住,它会报一下警,然后就挂了。此时,如果没有采用什么特别的方案来处理这个故障,DBA 很着急,重启数据库,但是数据库立马又被新的流量给打死了。

这就是缓存雪崩。

  • 雪崩的概念总结来说就是,大量的请求访问的都是不同的key,而这时候呢这些key都刚好过期,或者Redis宕机了,所有的请求直接打在了数据库上,导致了数据库的压力增大了

大约在 3 年前,国内比较知名的一个互联网公司,曾因为缓存事故,导致雪崩,后台系统全部崩溃,事故从当天下午持续到晚上凌晨 3~4 点,公司损失了几千万。

缓存雪崩的事前事中事后的解决方案如下:

  • 针对Redis宕机问题
    • Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。
    • Redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
  • 针对key过期问题
    • 时点性无关的数据:
      • redis 给key设置过期时间可以加一个随机数,保证大部分数据不会在同一时刻过期
      • 走击穿的策略,加锁,因为大量的请求里面可能会有很多是重复请求一个key的,这时候就可以加锁,只让一个请求穿过缓存去数据库查数据,然后插入缓存,其他的重复请求,后面再访问就有数据了
    • 时点性相关的数据:比如公司有一批数据必须在0点更新,然后存到缓存中,这时候并发请求又很大
      • 在业务层加判断,零点延时,只要到了十二点,每个请求就随机睡几十毫秒,做一个分流,不要所有的请求都在0点涌入,这样0点新数据更新到缓存中后就不会引起雪崩了
  • 针对数据库:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。

用户发送一个请求,系统 A 收到请求后,先查本地 ehcache 缓存,如果没查到再查 Redis。如果 ehcache 和 Redis 都没有,再查数据库,将数据库中的结果,写入 ehcache 和 Redis 中。

限流组件,可以设置每秒的请求,有多少能通过组件,剩余的未通过的请求,怎么办?走降级!可以返回一些默认的值,或者友情提示,或者空值。

好处:

  • 数据库绝对不会死,限流组件确保了每秒只有多少个请求能通过。
  • 只要数据库不死,就是说,对用户来说,2/5 的请求都是可以被处理的。
  • 只要有 2/5 的请求可以被处理,就意味着你的系统没死,对用户来说,可能就是点击几次刷不出来页面,但是多点几次,就可以刷出来了。

缓存穿透

  • 数据库当中压根不存在这个数据

对于系统 A,假设一秒 5000 个请求,结果其中 4000 个请求是黑客发出的恶意攻击。

黑客发出的那 4000 个攻击,缓存中查不到,每次你去数据库里查,也查不到。

举个栗子。数据库 id 是从 1 开始的,结果黑客发过来的请求 id 全部都是负数。这样的话,缓存中不会有,请求每次都“视缓存于无物”,直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。

  • 布隆过滤器
    • 弊端:不能删除,可以换成布谷鸟过滤器
  • 因为布隆过滤器会产生漏网之鱼,可以给请求存一个默认值存到Redis中,下次请求就去Redis中获取那个默认值,就不会去数据库了

module、布隆过滤器

  • 通过module引入,使用布隆过滤器解决缓存穿透问题

HashMap 的问题

  • 讲述布隆过滤器的原理之前,我们先思考一下,通常你判断某个元素是否存在用的是什么?应该蛮多人回答 HashMap 吧,确实可以将值映射到 HashMap 的 Key,然后可以在 O(1) 的时间复杂度内返回结果,效率奇高。但是 HashMap 的实现也有缺点,例如存储容量占比高,考虑到负载因子的存在,通常空间是不能被用满的,而一旦你的值很多例如上亿的时候,那 HashMap 占据的内存大小就变得很可观了。
  • 还比如说你的数据集存储在远程服务器上,本地服务接受输入,而数据集非常大不可能一次性读进内存构建 HashMap 的时候,也会存在问题。
  • 它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难
  • 原理解释
  • 如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值, 并对每个生成的哈希值指向的 bit 位置 1,例如针对值 “hello” 和三个不同的哈希函数分别生成了哈希值2、4、6,则上图转变为:
  • 我们现在再存一个值 “world”,如果哈希函数返回 3、4、8 的话,图继续变为:

  • 至此,下次再插入一个值“word”,通过hash算法定位出它的位置为‘3、4、8’,可以说明word可能已经插入到了集合中,为什么是可能,因为前面也看到了,‘hello’和‘world’两个值都出现了bit位为4的情况,如果数据量一多,把大部分的bit位都置为1了,那么下次再来一个值的时候,它的位置刚好都落在了置1的位上,但是实际这个值之前并没有出现过,所以布隆过滤器还是存在一定的误差,但是如果下次来一个值,它的位置当中有一位还是0,就可以很确切的说明它之前并没有出现过
  • 因此在redis中可以将布隆过滤器应用于防止缓存穿透,提前讲库中订单的id或者主键通过hash算法散落到bitmap集合中,当有人恶意用数据库中不存在的数据查询时,在bitmap中位置落到了0上,将可以直接将数据返回,就算好巧不巧的位置都落在1上,穿过redis到达数据库,数据库也可以返回一个空字符串,然后将它存到redis中,防止下次同一个字符串恶意请求。
  • 实现方式可以有很多种,因为redis是基于纯内存的,对CPU的损耗比较小,所以可以直接让redis集成这些,客户端只需要专注业务就行

缓存击穿

  • 某个热点key过期,或者突然大量请求访问原本非热点的数据

缓存击穿,就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。

不同场景下的解决方式可如下:

  • 首先,如果你可以很明确的知道,这个key一定是热点key,并且基本不会发生变化,则可尝试将该热点数据设置为永不过期。
    • redis是基于内存的缓存,内存自然有限,不适合大量的key设置永久不过期
  • 如果这个key以前根本不是热点key,但是突然的某一天一大堆请求进来,这又怎么预防呢?比如口罩,2019年末突然火爆
    • 加分布式锁,通过redis自身的setnx或者zookeeper的分布式锁。比如有一万个请求到达了redis要查口罩,这时候redis中刚好没这个缓存,如果这一万的并发请求都查不到去找数据库,势必会给数据库搞垮
    • 让这一万个并发请求肯定有第一个到达redis然后没查到数据的,就让他去获得锁的拥有权,然后其他剩下的并发请求也去获取同一把锁,拿到锁的才能去数据库拿数据,并且把数据写回redis后才释放锁,这样后面的请求就能在redis中找到数据了
    • 问题1:
      • 如果第一个获取锁的人挂了,就会造成死锁,可以采取给锁设置过期时间的策略
    • 问题2:
      • 如果第一个人只是超时了,超过了锁的过期时间,并没有挂
        • 可以采取多线程,一个线程去获取数据库数据
        • 另一个线程去监控数据是否取回,还没取回就更新锁的时间

总结

  • 数据库是架构的瓶颈,要尽可能的保证更多的有效请求到达数据库
  • 即便加大前置环节的复杂度
  • 不管是缓存穿透、击穿、还是雪崩,都会面临着并发请求的问题,也就是大量相同的无效请求到达数据库,虽然Redis内部是单线程的,但是这些相同的请求都会排队在Redis中,然后发现没数据再去数据库取数据然后再往回存这么一个过程,一直重复执行这个步骤,所以宁愿加大复杂度也不要让大量的无效请求到达数据库,可以采取加锁的形式,来保证在并发情况下,只有一个请求能抢到锁,执行上数步骤,其他请求因为没有获取到锁就不