缓存击穿是热点 key 过期瞬间大量请求穿透至数据库;须用分布式锁(如 set nx px)而非本地锁,加锁后需双重检查并设置新 ttl,避免误删锁和脏数据。

缓存击穿到底在击什么
缓存击穿不是缓存挂了,而是某个热点 key 刚过期的瞬间,大量并发请求同时发现缓存里没数据,全部穿透到数据库。比如秒杀商品详情页的 product:1001 在 TTL 到期那 1ms 内被 5000 个请求查到空,全打到 MySQL,DB 直接 CPU 拉满。
关键点在于:这不是缓存雪崩(多个 key 集体失效),也不是缓存穿透(查根本不存在的 key),它只针对单个高热度、有明确过期时间的 key。
为什么用分布式锁而不是本地锁
ReentrantLock 或 synchronized 在单机有效,但 Redis 缓存通常服务多台应用实例。如果 A 机器发现 product:1001 过期,加本地锁去查 DB 并回填缓存;B 机器同一毫秒也发现过期,它看不到 A 的锁,照样查 DB——锁就形同虚设。
必须用所有实例都能看到、能争抢的锁:
-
SET key random_value NX PX 30000是最轻量可靠的方案(NX=不存在才设,PX=30 秒自动释放) - 不要用
SETNX+EXPIRE两步,存在竞态:设成功但 expire 失败,锁永远不释放 - 释放锁必须校验 value(防止误删别人加的锁),用 Lua 脚本保证原子性:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end
加锁后怎么填缓存才不翻车
拿到锁 ≠ 可以直接查 DB。还要再检查一遍缓存,因为可能在你排队等锁时,前面的请求已经查完 DB 并写入缓存了。
- 先
GET key,命中则直接返回(双重检查) - 未命中,才执行 DB 查询 → 写缓存 → 释放锁
- 写缓存时务必设置新 TTL,别沿用旧值(旧值已过期),也别不设(变成永不过期,导致脏数据)
容易漏的细节:
- 锁超时时间(PX)要明显大于 DB 查询+写缓存的最大耗时,否则锁提前释放,又引发重复加载
- 如果 DB 查询失败(如超时、空结果),不能把 null 写进缓存(会变成缓存穿透),应按业务策略决定是否降级返回或抛异常
- 不建议在锁里做复杂逻辑(如调第三方 API),锁持有时间越短越好
要不要用 Redlock 或 Redisson
Redlock 理论上更“严谨”,但实际场景中,单 Redis 实例 + 正确的 SET 参数 + 合理的锁超时,已覆盖绝大多数业务。引入 Redlock 带来运维复杂度和延迟,却极少带来真实收益。
Redisson 的 RLock 封装了自动续期、看门狗机制,适合对锁可靠性要求极高、且团队熟悉其行为的项目。但要注意:
- 自动续期依赖心跳线程,若 JVM GC 停顿严重,可能误判锁失效
- 它的锁 key 默认带 UUID 和线程 ID,调试时在 Redis CLI 里不容易肉眼识别归属
真正影响数据库负载的,从来不是锁用得多精巧,而是锁粒度是否合理、缓存更新路径是否清晰、以及 DB 查询本身有没有优化。










