04-Redis 场景 缓存雪崩、缓存穿透、缓存预热、缓存降级(重要)

官网保平安:https://redis.io/

缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级等问题 ★★★★★

1.缓存雪崩 ★★★

缓存雪崩我们可以简单理解为:缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。

另外,缓存服务宕机也会导致缓存雪崩现象,导致所有的请求都落到了数据库上。

例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期,所有原本应该访问缓存的请求都去查询数据库了,而对数据库 CPU 和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,最终造成整个系统崩溃。

针对 Redis 服务不可用的情况:

  1. Redis 集群:采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
  2. 多级缓存:设置多级缓存,例如 本地缓存+Redis 缓存的二级缓存组合,当 Redis 缓存出现问题时,还可以从本地缓存中获取到部分数据。

针对大量缓存同时失效的情况:

  1. 设置随机失效时间(可选):为缓存设置随机的失效时间,例如在固定过期时间的基础上加上一个随机值,这样可以避免大量缓存同时到期,从而减少缓存雪崩的风险。并且选择合适的内存淘汰策略。
  2. 提前预热(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。
  3. 持久缓存策略(看情况):虽然一般不推荐设置缓存永不过期,但对于某些关键性和变化不频繁的数据,可以考虑这种策略。

其他:

  1. 大多数系统设计者考虑用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。
  2. 跑定时任务,在缓存失效前刷进新的缓存。
  3. 双缓存。我们有两个缓存,缓存 A 和缓存 B。缓存 A 的失效时间为20分钟,缓存 B 不设失效时间。自己做缓存预热操作。然后细分以下几个小点:
    1. 从缓存 A 读数据库,有则直接返回。
    2. A 没有数据,直接从 B 读数据,直接返回,并且异步启动一个更新线程。更新线程同时更新缓存 A 和缓存 B。

img

2.缓存击穿

缓存击穿中,请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期) 。这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。

缓存击穿

举个例子:秒杀进行过程中,缓存中的某个秒杀商品的数据突然过期,这就导致瞬时大量对该商品的请求直接落到数据库上,对数据库造成了巨大的压力。

解决办法

  1. 永不过期(不推荐):设置热点数据永不过期或者过期时间比较长。
  2. 提前预热(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。
  3. 加锁(看情况):在缓存失效后,通过设置互斥锁确保只有一个请求去查询数据库并更新缓存。
  4. 将过期时间组合写在 value 中,通过异步的方式不断的刷新过期时间,防止此类现象。

3.缓存穿透 ★★★

缓存穿透是指 用户查询数据时,在数据库里没有,自然在缓存中也不会有。这就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。

缓存穿透

解决办法:

  1. 最基本的是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。

  2. 最常见的是采用布隆过滤器,将所有可能存在的数据存到布隆过滤器中,一个一定不存在的数据可能会被布隆过滤器拦截掉,从而避免了对底层存储系统的查询压力。

  3. 还有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
    这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key。
    很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。

  4. 接口限流。
    根据用户或者 IP 对接口进行限流,对于异常频繁的访问行为,还可以采取黑名单机制,例如将异常 IP 列入黑名单。
    缓存击穿和雪崩都可以配合接口限流来解决,毕竟这些问题的关键都是有很多请求落到了数据库上造成数据库压力过大。

5TB 的硬盘上放满了数据,请写一个算法将这些数据进行去重。如果这些数据是一些 32bit 大小的数据该如何解决?如果是64bit 的呢?

  • Bitmap 典型的就是哈希表。缺点是 Bitmap 对于每个元素只能记录 1bit 信息,如果还想完成额外的功能,只能靠牺牲更多的空间、时间来完成了,如使用 2bit 记录一个元素,其中 1bit 记录是否存在,1bit 记录其他信息。

  • 布隆过滤器(Bloom-Filter):
    Bloom-Filter 算法的核心思想就是利用多个不同的 Hash 函数来解决“冲突”。通过引入 k(k>1) 个相互独立的哈希函数,保证在给定的空间、误判率下,完成元素判重的过程。
    它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
    Hash 存在一个冲突(碰撞)的问题,用同一个 Hash 得到的两个URL的值有可能相同。为了减少冲突,我们可以多引入几个 Hash,如果通过其中的一个 Hash 值我们得出某元素不在集合中,那么该元素肯定不在集合中。只有在所有的 Hash 函数告诉我们该元素在集合中时,才能确定该元素存在于集合中。这便是 Bloom-Filter 的基本思想。
    Bloom-Filter 一般用于在大数据量的集合中判定某元素是否存在。

4.缓存预热

缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

缓存预热思路

  • 数据量不大时,可以在项目启动的时候自动进行加载。
  • 直接写个缓存刷新页面,上线时手工访问页面。
  • 写个定时任务,定时刷新缓存。
  • 使用消息队列,比如 Kafka,来异步地进行缓存预热,将数据库中的热点数据的主键或者 ID 发送到消息队列中,然后由缓存服务消费消息队列中的数据,根据主键或者 ID 查询数据库并更新缓存。

5.缓存降级

当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心服务的性能时,仍然需要保证核心服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。

降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。可以以参考日志级别设置预案:

  1. 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
  2. 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
  3. 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
  4. 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。

服务降级的目的,是为了防止 Redis 服务故障,导致数据库跟着一起发生雪崩问题。因此对于不重要的缓存数据,可以采取服务降级策略。

一个比较常见的做法就是,Redis 出现问题,不去数据库查询,而是直接返回默认值给用户。

热点数据和冷数据

缓存热点数据,缓存才有价值。

对于冷数据,大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不大。

对于热点数据,比如某 IM 产品,生日祝福模块,当天的寿星列表,缓存以后可能读取数十万次。再举个例子,某导航产品,我们将导航信息,缓存以后可能读取数百万次。

数据更新前至少读取两次 缓存才有意义。这个是最基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。

那存不存在,修改频率很高,但是又不得不考虑缓存的场景呢?有!比如我们的某助手产品的,点赞数,收藏数,分享数等是非常典型的热点数据,但是又不断变化,此时就需要将数据同步保存到 Redis 缓存,减少数据库压力。


04-Redis 场景 缓存雪崩、缓存穿透、缓存预热、缓存降级(重要)
https://flepeng.github.io/interview-41-数据库-41-Redis-04-Redis-场景-缓存雪崩、缓存穿透、缓存预热、缓存降级(重要)/
作者
Lepeng
发布于
2020年8月8日
许可协议