缓存击穿是热点key过期瞬间高并发查询导致DB压力飙升,需布隆过滤器前置拦截非法请求、空值缓存兜底短暂窗口、逻辑过期保障合法热点数据不被穿透,三者协同防御。

缓存击穿的本质是热点 key 过期瞬间的并发穿透
不是所有“查不到缓存就打 DB”的情况都叫击穿——只有数据库里**确实存在该数据**,但 Redis 中的 key **恰好过期 + 高并发访问**,才构成击穿。此时大量线程同时发现缓存 miss,又几乎同时去查 DB、回写缓存,造成瞬时 DB 压力飙升。
单纯加锁或延长 TTL 只解决“并发重建”问题,但没拦住**本不该存在的 key 请求**(比如恶意 ID、已下架商品 ID)。这类请求哪怕加了互斥锁,也会反复触发无效 DB 查询 + 写空值,最终污染缓存、挤占内存。所以必须前置过滤 + 后置兜底协同防御。
布隆过滤器必须放在缓存读取之前,且只校验“一定不存在”的 key
布隆过滤器(Bloom Filter)不是万能钥匙,它只保证:如果 bloom.mightContain(key) 返回 false,那这个 key 在底层数据源中**绝对不存在**;若返回 true,只是“可能存在”,仍需走缓存 → DB 流程。
常见错误是把它当缓存用,或者在缓存 miss 后才查布隆——这就失去意义了。正确位置是:请求一进来,先过布隆:
- 对所有查询入口(如
GET /product/{id}),解析出id后立即调用bloom.mightContain("product:" + id) - 返回
false→ 直接返回 404 或空对象,**不查 Redis、不查 DB、不写任何缓存** - 返回
true→ 正常走redis.get("product:" + id)流程 - 布隆本身要定期重建(如每天凌晨同步全量有效 ID),并支持热更新(如新增商品时实时
bloom.put())
空值缓存 TTL 必须短,且真实数据写入时要主动删除空值 key
即使有布隆过滤,仍可能漏掉刚新增但尚未同步进布隆的合法 key(如秒杀上架后几毫秒内请求),所以空值缓存仍是必要兜底。但它的 TTL 不能拍脑袋设 60 秒或 5 分钟——得看业务容忍脏读的窗口和 DB 压力曲线。
关键陷阱在于:很多人缓存空值后,等 DB 插入真实数据,却忘了删掉那个 "NULL" key,导致后续请求永远命中空值,变成“假击穿”:
- 空值写入统一用
redis.setex(key, 60, "NULL")(TTL 60 秒是较安全起点,可按压测结果下调) - 任何写 DB 的路径(
insert/update/restore),只要涉及该 key 对应实体,必须同步执行redis.del(key) - 不要依赖“空值自然过期”,因为高并发下它可能被反复写入,实际存活远超预期
热点 key 永不过期 + 逻辑过期字段才是抗击穿的稳定组合
对真正高频访问的热点 key(如首页 banner、秒杀商品详情),靠布隆 + 空值缓存还不够——它们本就不该过期。但“永不过期”不等于“永不更新”,而是把过期判断从 Redis 层移到应用层:
示例结构:{"data": {...}, "expireAt": 1744256580}(Unix 时间戳)
- 写入时:用
redis.set("product:1001", jsonStr),不带 TTL - 读取时:反序列化后检查
expireAt - 过期了?尝试
redis.setnx("mutex:product:1001", "1", 30)获取锁,成功则异步刷新;失败则直接返回旧数据 - 注意:Redis 内存淘汰策略必须设为
allkeys-lru或volatile-lru,否则永不过期 key 可能被误删
布隆过滤器防的是“非法请求”,逻辑过期保的是“合法热点”,空值缓存兜的是“短暂窗口期”。三者缺一不可,而最容易被忽略的,是布隆未覆盖的新数据上线间隙,以及空值 key 被遗忘清理。


















