缓存击穿指单个热点key过期瞬间遭大量并发请求穿透至数据库,导致瞬时db压力激增;核心解决思路是“续命”(如永不过期、自动续期)或“防并发重建”(如原子化set+ex、互斥锁),布隆过滤器对此无效。

缓存击穿(Cache Breakdown)指某个热点 key 在过期瞬间被大量并发请求穿透到数据库,造成 DB 瞬时压力激增。它和缓存穿透、雪崩不同,**只针对单个高热度 key 的过期场景**。解决它的核心思路不是“挡流量”,而是“续命”或“防并发重建”。没有绝对“更优”的方案,只有在具体场景下更稳、更轻、更易维护的选型。
用 SETNX + 过期时间实现互斥重建,为什么容易漏掉 EXPIRE?
这是最常被抄错的写法:先 SETNX lock:hotkey 1,成功再查 DB、写缓存、删锁。但很多人忘了给缓存本身设过期时间,导致后续所有请求都走 DB —— 因为缓存没写进去,或者写进去了但没 EX 参数。
正确做法必须两步原子化(或至少强顺序):
-
SET hotkey value EX 300 NX—— 直接带过期时间的原子写入,避免写完不设过期 - 若失败,才去争锁:
SETNX lock:hotkey 1,成功后加EXPIRE lock:hotkey 10防死锁,再查 DB、SET hotkey value EX 300、DEL lock:hotkey - 客户端需统一处理
nil响应:不直接报错,而是降级查 DB 或返回默认值
Redis 2.6+ 的 EVAL 脚本能否真正解决并发重建?
可以,但代价是 Lua 脚本阻塞主线程 —— 所有其他命令(包括 GET、INCR)都会排队等它执行完。对单个热点 key 来说,脚本内逻辑越重(比如含 HTTP 请求或复杂计算),阻塞越久,QPS 下跌越明显。
典型误用:
redis.call("GET", KEYS[1])
if not res then
local data = http.get(...) -- ❌ 不允许!Redis Lua 不支持网络 IO
redis.call("SET", KEYS[1], data, "EX", 300)
end
可行的轻量脚本只做“判断 + 写缓存”,DB 查询必须由客户端完成:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 脚本只检查 key 是否存在,不存在则
SET ... NX EX占位,并返回 1;存在则返回 0 - 客户端收到 1 后去查 DB,再
SET真实值;收到 0 则直接GET - 注意:脚本不能依赖
redis.call("TTL", ...)判断是否“即将过期”,因为 TTL 返回 -2/-1 时语义模糊,不如业务层控制过期前 10 秒主动刷新
布隆过滤器(Bloom Filter)对缓存击穿完全无效,别乱套用
布隆过滤器解决的是**缓存穿透**(查一个压根不存在的 key),它能快速告诉你“这个 key 绝对不存在”,从而避免查 DB。但缓存击穿的前提是 key 存在且热度极高 —— 它只是刚好过期了。此时布隆过滤器永远返回 true,起不到任何拦截或分流作用。
如果你在击穿场景里加了布隆过滤器,只会徒增一次 Redis GETBIT 调用,还可能因 filter 误判(false positive)导致合法请求被拒。
真正该用布隆过滤器的地方只有两个:
- 用户输入的 ID 是纯数字但长度不定(如短链 ID),且存在大量恶意构造的非法 ID
- 黑名单/白名单类场景,需要极低内存开销判断海量元素是否“可能在集合中”
本地缓存(Caffeine/Guava)+ Redis 双检,为什么反而增加复杂度?
双检(先查本地缓存,再查 Redis,再查 DB)看似保险,但在击穿场景下问题突出:
- 本地缓存过期时间难设定:设太短,频繁穿透;设太长,无法响应 Redis 中 key 的真实过期变化
- 多实例部署时,各节点本地缓存不同步,A 节点刚重建好 hotkey,B 节点仍认为过期,又触发一次 DB 查询
- 本地缓存失效策略(如 LRU)可能把 hotkey 淘汰掉,而 Redis 还没过期,造成“伪击穿”
除非你的服务是单实例、且 hotkey 更新频率极低(如每天一次)、且能接受秒级不一致,否则不建议引入本地缓存对抗击穿。
真正关键的细节常被忽略:**击穿防护的有效性,取决于你如何定义“热点 key 的生命周期”**。如果业务上允许该 key 提前 5 秒自动续期(比如用 EXPIRE hotkey 300 在每次访问时刷新),那比任何锁或脚本都简单可靠;但如果 key 必须严格按业务时间过期(如支付订单 15 分钟未支付自动关闭),那就必须接受重建瞬间的 DB 压力,并靠限流 + 降级兜底 —— 此时所谓“最优方案”其实是“最可控的兜底方案”。










