缓存击穿需用redis分布式锁(如setnx)实现互斥,因单机锁在集群中无效;核心流程含双重检查、带前缀锁key、ex过期、lua安全释放及休眠重试,避免脏数据与雪崩。

高并发下热点 key 过期瞬间引发缓存击穿,核心是让“只有一个请求查库并回填”,其余请求等结果——这必须靠分布式互斥锁,单机锁(synchronized、ReentrantLock)在集群中完全无效。
为什么非得用 Redis 的 SETNX 做锁
因为服务通常多实例部署,每个 JVM 的本地锁彼此隔离。10 台机器同时发现缓存 miss,每台都可能去查 DB,击穿照旧发生。Redis 是共享中间件,天然适合当“裁判”:谁先 SETNX lock:goods:1001 "1" EX 30 成功,谁就获得唯一加载权。
加锁的关键操作细节
-
锁 key 必须带业务前缀,比如
mutex:goods:1001,避免不同商品误删彼此的锁 - 必须设过期时间(EX),如 30 秒,防止服务宕机或异常导致锁永远不释放
- 释放锁必须用 Lua 脚本,先判断 key 存在且值匹配再删,否则 A 加锁、B 异常删锁、A 回填失败,就会出现脏数据
- 抢锁失败后不能立即重试,建议休眠 50–100ms,避免大量线程高频轮询压垮 Redis
完整流程不能漏双重检查
拿到锁之后,不能直接查库,要再查一次缓存——因为可能有其他线程已抢先完成回填。标准流程是:
- 查缓存 → 有则返回
- 无则尝试
SETNX抢锁 → 失败则休眠后重试 - 抢锁成功 → 再查一次缓存(双重检查)→ 仍有则直接返回
- 确认无缓存 → 查 DB → 写入缓存(注意设合理过期时间)→ 安全释放锁
容易翻车的三个盲区
-
空值没统一标识:DB 返回 null、空字符串、"{}"、"null" 字符串,都应转为一个占位对象(如
NULL_CACHE),否则会反复触发锁逻辑 - 锁超时太短又没续期:DB 查询耗时 200ms,但锁只设 200ms,刚好卡在回填前过期,多个线程又同时进 DB
-
回填缓存没设过期时间或设得太死:比如写死
EX 86400,后续 DB 更新后缓存无法自动失效,造成脏读











