缓存穿透、击穿、雪崩:三大经典缓存问题一次讲清
# T
为什么要分清这三个词
面试和故障复盘里,"缓存穿透""缓存击穿""缓存雪崩"经常被混着说。它们确实都描述"请求绕过缓存打到数据库",但成因和解法完全不同。用一句话区分:
- 穿透:查的是根本不存在的数据,缓存永远不会有
- 击穿:某个热点 key 恰好过期,瞬间大量请求直击数据库
- 雪崩:大批 key 同时失效(或缓存服务本身宕机),数据库被洪峰冲垮
一个查"不存在的用户",一个查"秒杀商品恰好过期",一个是"整片缓存集体阵亡"——场景不同,药方也不同。
缓存穿透:查询不存在的数据
成因:请求的参数在数据库里根本没有对应记录(典型如恶意伪造 ID 遍历攻击)。缓存遵循"查不到就不写",于是每次请求都穿透到数据库。数据库本来有空值保护吗?没有——它每次都要执行真实查询并返回空。
解法一:缓存空值
查数据库得到空 → 仍然写入缓存,值为 NULL,设置较短 TTL(如 60 秒)。后续同类请求直接命中空值返回,不再访问数据库。
代价是缓存里存了些无意义条目,配合短 TTL 和参数合法性校验(如 ID 格式、范围检查)可以把垃圾数据压到很低。
解法二:布隆过滤器
在缓存前放一层布隆过滤器:把所有合法 key 预先写入,请求先问过滤器——"这个 key 可能存在吗"。布隆过滤器说"不存在"就一定不存在,直接拦截;说"可能存在"才走缓存和数据库。
特点是空间效率极高(几亿 key 也只需几百 MB),代价是有一定误判率且标准实现不支持删除。适合 key 集合相对稳定的场景。
缓存击穿:单个热点 key 过期
成因:某个被高频访问的 key(如首页爆款商品)到达过期时间的瞬间,成千上万并发请求同时发现缓存失效,全部涌向数据库执行同一个查询。
解法一:互斥锁,只放一个请求去回源
key 失效时:第一个拿到分布式锁(如 SETNX)的请求去查数据库并回填缓存;其他请求短暂自旋等待(如 50ms 重试一次);缓存回填后锁释放,后续请求全部命中缓存。
数据库只承受一次查询压力,代价是等待期间少数请求延迟略增。
解法二:逻辑过期,物理上永不过期
热点 key 不设 TTL,值里额外存一个"逻辑过期时间"。读取时发现逻辑过期,由一个异步线程负责更新缓存,当前请求先返回旧值。优点是完全不阻塞;缺点是短期内部可能返回略旧的数据,适合能容忍短暂不一致的场景。
两种方案选型看业务:一致性敏感选互斥锁,可用性敏感选逻辑过期。
缓存雪崩:大批 key 集体失效
成因:两种典型情况——一是给大量 key 设了相同的 TTL,同一时刻集体过期;二是缓存服务自身宕机,所有请求被迫落到数据库。后者冲击力远大于前两者。
解法一:TTL 加随机偏移
过期时间 = 基础值 + random(0, 300)秒。一行代码把"集体失效"摊薄成"缓慢失效",成本几乎为零,是最应该无条件做的基础防护。
解法二:多级缓存
本地缓存(进程内)作为第一层,分布式缓存作为第二层。分布式缓存抖动时,本地缓存还能顶住一部分读流量,给恢复争取时间。
解法三:缓存服务高可用 + 熔断降级
Redis 用主从 + 哨兵或集群模式避免单点;应用侧对数据库访问加限流熔断——缓存挂了时宁可返回部分旧数据或降级页面,也不能让数据库被洪峰打死。数据库一旦被打挂,恢复时间远长于缓存。
三者对照速查表
| 问题 | 触发条件 | 核心解法 | 一句话记忆 |
|---|---|---|---|
| 穿透 | 数据不存在 | 空值缓存 / 布隆过滤器 | 查无此数据 |
| 击穿 | 单个热点 key 过期 | 互斥锁 / 逻辑过期 | 精确打击 |
| 雪崩 | 大批 key 同时失效或缓存宕机 | TTL 随机化 / 多级缓存 / 熔断降级 | 全面崩塌 |
小结
三个问题的本质都是"缓存没有兜住请求",差异只在范围:穿透是数据不存在,击穿是单点过期,雪崩是面状失效。实践中它们常常叠加出现——一次事故里既可能有恶意穿透,也可能恰好赶上批量过期。所以生产系统的标准做法是组合拳:入口做参数校验和布隆过滤器防穿透,热点 key 用逻辑过期防击穿,全局 TTL 随机化加熔断降级防雪崩。缓存体系扛得住异常,数据库才能真正高枕无忧。
- 点赞
- 收藏
- 关注作者
评论(0)