缓存穿透、缓存击穿、缓存雪崩
缓存穿透
缓存穿透是指查询一个根本不存在的数据。由于缓存中不存在(未命中),这个请求会直接穿透缓存层,到达数据库。如果同时有大量这样的请求(比如恶意攻击、爬虫扫描),会给数据库造成巨大压力,甚至可能压垮数据库。
核心原因: 数据在缓存和数据库中都不存在。
解决方案:
缓存空对象: 即使从数据库没查到,也把一个空值(比如
null)或特殊标记存入缓存,并设置一个较短的过期时间(例如 5分钟)。后续同样的请求就会命中缓存,拿到这个空值,从而保护数据库。- 缺点: 如果恶意攻击者用大量不同的 key 来攻击,会导致缓存中存储大量无用的空值,浪费内存。
布隆过滤器: 这是解决此问题的最优方案。在访问缓存和数据库之前,先将所有可能存在的 key 哈希到一个巨大的位图(BitMap)中。
- 流程: 收到请求 -> 先查布隆过滤器 -> 如果过滤器说肯定不存在,则直接返回空,不再访问缓存和数据库 -> 如果过滤器说可能存在,再继续走正常的缓存流程。
- 优点: 内存占用极小,能高效判断一个元素是否绝对不存在于某个集合中。
- 缺点: 有极低的误判率(可能会把不存在的 key 判为存在,但绝不会把存在的 key 判为不存在),但对于防护场景可以接受。
缓存击穿
缓存击穿是指某一个热点 key(非常热门的数据)在缓存过期的瞬间,同时有大量的请求对这个 key 发起请求。这些请求发现缓存失效,都会去加载数据库数据并回设缓存,这个过程中大量的并发请求会瞬间给数据库带来巨大的冲击压力。
核心原因: 某个热点 key 过期,同时有高并发请求。
解决方案:
- 永不过期: 对真正的极品热点数据,可以设置逻辑过期,而不是物理过期。即在缓存 value 中存储一个过期时间字段,程序读取时判断是否过期,如果过期则触发异步更新,而不是让缓存自己失效。
- 互斥锁: 这是最常用的方法。当发现缓存失效时,不是所有线程都去查数据库,而是使用分布式锁(如 Redis 的
SETNX),只允许一个线程去查询数据库并重建缓存,其他线程等待,缓存重建完成后,其他线程再从缓存中获取数据。- 优点: 能很好地保护数据库。
- 缺点: 用户体验上可能会有一点点延迟(等待锁)。
缓存雪崩
缓存雪崩是指在同一时间,缓存中大量的 key 同时过期或者 Redis 缓存服务直接宕机,导致所有请求都无法从缓存中获取数据,全部转发到数据库,造成数据库瞬时压力过大而崩溃,就像雪崩一样连锁反应。
**核心原因:大量 key 同时失效 或 缓存服务挂掉。
解决方案:
- 设置不同的过期时间: 这是预防同时过期的最有效方法。给缓存数据的过期时间加上一个随机值(比如基础时间 + 一个随机数),让 key 的过期时间尽可能分散开,避免大量 key 在同一时刻失效。
- 构建高可用的缓存集群: 通过 Redis 的哨兵(Sentinel)或集群(Cluster)模式,实现缓存服务的高可用,即使个别节点宕机,整个服务依然可用,防止服务挂掉导致的雪崩。
- 服务降级和熔断: 在系统层面,当检测到数据库压力过大时,可以采用降级策略,比如对于非核心数据,直接返回预定义的默认值或错误页面,减轻数据库的压力。使用 Hystrix 等工具实现熔断。
- 提前预热: 在系统高峰期来临之前(如零点大促前),提前把热点数据加载到缓存中,并合理设置它们的过期时间。
总结对比
| 问题类型 | 核心原因 | 问题本质 | 解决方案 |
|---|---|---|---|
| 缓存穿透 | 查询不存在的数据 | 缓存和数据库都没有,请求直达数据库 | 1. 缓存空对象 2. 布隆过滤器(最优) |
| 缓存击穿 | 单个热点key过期 + 高并发 | 单个key失效,并发量大 | 1. 互斥锁 2. 逻辑过期(永不过期) |
| 缓存雪崩 | 大量key同时过期 或 缓存服务宕机 | 大规模缓存失效,系统级故障 | 1. 过期时间加随机值 2. 高可用集群 3. 服务降级熔断 4. 数据预热 |
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 技术之路!
评论


