「缓存三兄弟及解决相关方案」
来自 gpt 的摘要: 缓存雪崩是指大量缓存数据在同一时间失效,导致大量请求直接打到数据库,造成数据库压力骤增,甚至宕机。解决方案包括:通过Redis集群避免单点故障,设置多级缓存和缓存降级,给缓存设置随机过期时间和缓存预热等。缓存穿透是指请求的数据在缓存和数据库中都不存在,所有请求直接打到数据库。常用应对方法有:参数校验、缓存无效key、使用布隆过滤器、以及接口限流。缓存击穿是指高并发请求集中访问某个热点数据,而该数据在缓存中失效,导致大量请求同时打到数据库。解决办法包括设置热点数据永不过期、缓存预热和使用互斥锁防止并发读写数据库。复杂场景如Redis集群和缓存降级涉及更多细节,布隆过滤器则能有效防止大量无效请求。
一、缓存雪崩
场景
缓存雪崩这个场景比较少见,但一出现就是比较致命的问题,可以理解为:「同一时间」缓存中的「大面积数据失效」,导致「大量请求」直接打在数据库,对数据库 CPU 和内存造成巨大压力,严重的会造成数据库宕机。一般导致缓存雪崩现象有两个原因:「(1)缓存服务宕机(2)缓存数据大面积同时过期。」
解决方案
针对缓存服务不可用
- 增强服务可用性 ->「Redis 集群」:通过Redis集群实现高可用,避免单节点宕机造成的影响。
- 预防服务不可用 ->「缓存降级」:在预见到可能的负载增加时,主动降级非核心服务,如返回默认值或简化数据返回,以降低 Redis 依赖,防止 Redis 宕机。
- 服务不可用的兜底 -> 多级缓存:设置多级缓存,例如本地缓存 + Redis 缓存的二级缓存组合,当 Redis 缓存出现问题时,还可以从本地缓存中获取到部分数据。
针对缓存数据大面积同时过期
- 解决「同一时间」的问题 -> 设置随机失效时间:为缓存设置随机的失效时间,例如在固定过期时间的基础上加上一个随机值,这样可以避免大量缓存同时到期,从而减少缓存雪崩的风险。
- 解决「大面积数据失效」的问题 -> 持久缓存策略(看情况):虽然一般不推荐设置缓存永不过期,但对于某些关键性和变化不频繁的数据,可以考虑这种策略。
- 解决「大面积数据失效」的问题 ->「缓存预热」:在高访问量时段之前,将热点数据提前加载到缓存中,避免流量高峰期缓存失效导致的压力。
- 解决「大量请求」的问题 -> 对数据库操作加互斥锁或队列:保证不会有大量线程对数据库同时进行读写,避免数据库受到巨大压力。
- 解决「大量请求」的问题 ->「接口限流」:控制每秒请求量,减少瞬时高并发对数据库的直接冲击,降低流量峰值的影响。
二、缓存穿透
场景
缓存穿透可以理解为:请求「大量不存在」于缓存和数据库中的数据,导致所有请求直接打到数据库,并「没有经过缓存」,对数据库 CPU 和内存造成巨大压力,严重的会造成数据库宕机。这种场景常见于「黑客恶意攻击」和「业务操作问题」导致的。
解决方案
- 在接口之前阻挡 ->「接口限流」:通过对IP或用户限流,减少恶意或异常高并发访问,降低峰值流量。
- 在缓存之前阻挡 -> 参数校验:做好后端服务的参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。
- 在数据库之前阻挡 ->「布隆过滤器」(推荐):把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会继续流程。
- 防止再穿透 -> 缓存无效 key:对查询未命中的 key 写入缓存空值,防止重复查询该 key 引发攻击,适用于请求变化不频繁的场景,需要注意的是,一般我们将 TTL 设置较小,防止占用过多内存。
三、缓存击穿
场景
缓存击穿可以理解为:正在接受「大量请求」的一个热点数据刚好在缓存「失效」,导致这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库 CPU 和内存造成巨大压力,严重的会造成数据库宕机。一般这种情况的原因是「热点数据过期」。
解决方案
- 解决「大量请求」问题 -> 对数据库操作加互斥锁或队列:保证不会有大量线程对数据库同时进行读写,避免数据库受到巨大压力。
- 解决「失效」问题 -> 永不过期(不推荐):设置热点数据永不过期或者过期时间比较长。
- 解决「失效」问题 ->「缓存预热」:针对热点数据提前预热,将其存入缓存中并设置合理的过期时间,比如秒杀场景下的数据在秒杀结束之前不过期。
四、复杂方案的具体实现
Redis 集群
方案较复杂,后期更新。
缓存降级
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
可以参考日志级别设置预案:(1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;(2)警告:有些服务在一段时间内成功率有波动(如在 95~100%之间),可以自动降级或人工降级,并发送告警;(3)错误:比如可用率低于 90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;(4)严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
服务降级的目的,是为了防止 Redis 服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis 出现问题,不去数据库查询,而是直接返回默认值给用户。
缓存预热
- 使用「定时任务」,比如 xxl-job,来定时触发缓存预热的逻辑,将数据库中的热点数据查询出来并存入缓存中。
- 使用「消息队列」,比如 Kafka,来异步地进行缓存预热,将数据库中的热点数据的主键或者 ID 发送到消息队列中,然后由缓存服务消费消息队列中的数据,根据主键或者 ID 查询数据库并更新缓存。
布隆过滤器
了解过布隆过滤器吗?
布隆过滤器它的数据结构其实是位图,我们会对要放入的数据进行多次 hash,然后放到对应的位上并设置为 1,后面的数据要判断是否存在的时候,只需要进行多次 hash,如果对应的位均为 1 的时候数据可能是存在的,如果不都为 1 那代表数据「一定不存在」。虽然会误判,但不会漏判,可以有效地过滤大量无效请求。
布隆过滤器如何使用?
把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。
布隆过滤器有没有什么缺点?
它占用空间更少并且效率更高,但是缺点是其返回的结果是「概率性」的,而不是非常准确的。理论情况下添加到集合中的「元素越多,误报的可能性就越大」。并且,存放在布隆过滤器的数据「不容易删除」。
布隆过滤器误判会对业务造成什么影响?
布隆过滤器的「误判」是指它可能会「判断某个实际不存在的元素为存在」。具体到业务中,布隆过滤器的误判可能会带来的影响包括:
误缓存空值:布隆过滤器可能会认为某些不存在的数据存在于系统中,从而避免了对数据库的查询,但实际上这部分数据并不存在。因此,系统可能会缓存空值或默认值,而不是从数据库获取真实数据。虽然这不会对数据库产生额外压力,但可能会导致用户看到错误或不完整的数据。
影响用户体验:当布隆过滤器误判时,系统可能返回不正确的数据或不进行应有的查询操作,导致用户看到不符合预期的结果,从而影响用户体验。
- 业务逻辑异常:在某些场景中,业务逻辑可能会依赖于查询结果的准确性,如果布隆过滤器误判,导致系统未能查询到正确的数据,可能会触发错误的业务逻辑或决策。
不过,布隆过滤器「不会出现漏判」,也就是说它不会错过那些实际存在的元素。因此,误判带来的影响虽然存在,但相对于防止缓存穿透的好处,布隆过滤器依然是一个非常有效的解决方案。
服务限流
五、相关问题
缓存雪崩和缓存击穿有什么区别?
缓存雪崩和缓存击穿比较像,但导致缓存雪崩的原因是缓存中的大量数据失效,缓存击穿导致的原因主要是某个热点数据不存在与缓存中(通常是因为缓存中的那份数据已经过期)。
怎么保证多级缓存和 redis 缓存双写的数据一致性?
其实类似数据库和 redis 一样,都是先更新数据库在删除缓存,这里我们把缓存看成本地缓存,数据库看成 redis 就行了。可能会出现数据不一致的问题,但这种情况比较极端,我们可以把二级缓存的过期时间设计小一点。
多级缓存在多节点怎么保证数据的一致性?
这个感觉不太现实,既然使用了二级缓存你就要去容忍它不能保证一致性的缺点,所以基于这点思考我们要让二级缓存过期时间尽可能短。还是有方案来保证数据的一致性,就在对某个二级缓存更新后,通知其他节点,但是这会造成很没必要的资源损耗,我认为是不考虑的,我们可以选择容忍。
参考:在多节点的多级缓存架构中,缓存一致性是一个复杂但重要的问题。常见的几种解决方案和策略包括:
缓存失效策略:简单直接,但在高并发和频繁写入的场景下有性能问题。
发布/订阅更新通知:能够快速传播缓存更新,减少不一致的窗口时间。
使用分布式缓存:如 Redis 集群,可以天然解决一致性问题,但需要更复杂的管理和配置。
一致性哈希算法:适用于大型分布式系统,能有效减少不一致性,但实现复杂。
分布式锁机制:保证强一致性,但性能会有所下降,适用于高一致性需求场景。
双写检测与补偿机制:在不一致发生时触发补偿,适合高可用系统。
具体选择哪种策略要根据系统的性能需求、业务场景的并发情况以及对数据一致性的要求来决定。
