缓存穿透

缓存穿透是指查询一个根本不存在的数据。由于缓存中不存在(未命中),这个请求会直接穿透缓存层,到达数据库。如果同时有大量这样的请求(比如恶意攻击、爬虫扫描),会给数据库造成巨大压力,甚至可能压垮数据库。

核心原因: 数据在缓存和数据库中都不存在

解决方案:

  1. 缓存空对象: 即使从数据库没查到,也把一个空值(比如 null)或特殊标记存入缓存,并设置一个较短的过期时间(例如 5分钟)。后续同样的请求就会命中缓存,拿到这个空值,从而保护数据库。

    • 缺点: 如果恶意攻击者用大量不同的 key 来攻击,会导致缓存中存储大量无用的空值,浪费内存。
  2. 布隆过滤器: 这是解决此问题的最优方案。在访问缓存和数据库之前,先将所有可能存在的 key 哈希到一个巨大的位图(BitMap)中。

    • 流程: 收到请求 -> 先查布隆过滤器 -> 如果过滤器说肯定不存在,则直接返回空,不再访问缓存和数据库 -> 如果过滤器说可能存在,再继续走正常的缓存流程。
    • 优点: 内存占用极小,能高效判断一个元素是否绝对不存在于某个集合中。
    • 缺点: 有极低的误判率(可能会把不存在的 key 判为存在,但绝不会把存在的 key 判为不存在),但对于防护场景可以接受。

缓存击穿

缓存击穿是指某一个热点 key(非常热门的数据)在缓存过期的瞬间,同时有大量的请求对这个 key 发起请求。这些请求发现缓存失效,都会去加载数据库数据并回设缓存,这个过程中大量的并发请求会瞬间给数据库带来巨大的冲击压力。

核心原因: 某个热点 key 过期,同时有高并发请求。

解决方案:

  1. 永不过期: 对真正的极品热点数据,可以设置逻辑过期,而不是物理过期。即在缓存 value 中存储一个过期时间字段,程序读取时判断是否过期,如果过期则触发异步更新,而不是让缓存自己失效。
  2. 互斥锁: 这是最常用的方法。当发现缓存失效时,不是所有线程都去查数据库,而是使用分布式锁(如 Redis 的 SETNX),只允许一个线程去查询数据库并重建缓存,其他线程等待,缓存重建完成后,其他线程再从缓存中获取数据。
    • 优点: 能很好地保护数据库。
    • 缺点: 用户体验上可能会有一点点延迟(等待锁)。

缓存雪崩

缓存雪崩是指在同一时间,缓存中大量的 key 同时过期或者 Redis 缓存服务直接宕机,导致所有请求都无法从缓存中获取数据,全部转发到数据库,造成数据库瞬时压力过大而崩溃,就像雪崩一样连锁反应。

**核心原因:大量 key 同时失效缓存服务挂掉

解决方案:

  1. 设置不同的过期时间: 这是预防同时过期的最有效方法。给缓存数据的过期时间加上一个随机值(比如基础时间 + 一个随机数),让 key 的过期时间尽可能分散开,避免大量 key 在同一时刻失效。
  2. 构建高可用的缓存集群: 通过 Redis 的哨兵(Sentinel)或集群(Cluster)模式,实现缓存服务的高可用,即使个别节点宕机,整个服务依然可用,防止服务挂掉导致的雪崩。
  3. 服务降级和熔断: 在系统层面,当检测到数据库压力过大时,可以采用降级策略,比如对于非核心数据,直接返回预定义的默认值或错误页面,减轻数据库的压力。使用 Hystrix 等工具实现熔断。
  4. 提前预热: 在系统高峰期来临之前(如零点大促前),提前把热点数据加载到缓存中,并合理设置它们的过期时间。

总结对比

问题类型 核心原因 问题本质 解决方案
缓存穿透 查询不存在的数据 缓存和数据库都没有,请求直达数据库 1. 缓存空对象
2. 布隆过滤器(最优)
缓存击穿 单个热点key过期 + 高并发 单个key失效,并发量大 1. 互斥锁
2. 逻辑过期(永不过期)
缓存雪崩 大量key同时过期 或 缓存服务宕机 大规模缓存失效,系统级故障 1. 过期时间加随机值
2. 高可用集群
3. 服务降级熔断
4. 数据预热