用 setnx 加分布式锁是最直接的解法:通过原子性设置带业务前缀和过期时间的锁(如 lock:product:10086),配合 lua 脚本安全释放,可有效防止缓存击穿导致的数据库雪崩。

用 SETNX 加分布式锁是最直接的解法
缓存击穿本质是:一个热门 key 过期瞬间,大量请求同时穿透到数据库。不加控制的话,DB 就会收到几百上千次重复查询——哪怕只持续 1 秒,也足以拖垮服务。
用 SETNX(即 SET if Not eXists)在 Redis 里设一个临时锁是最常用、最轻量的方案。它天然支持原子性,且不需要额外组件。
- 锁名建议带业务前缀和
key,比如lock:product:10086,避免不同业务误删 - 必须设置过期时间(
EX参数),否则锁进程崩溃或异常退出会导致死锁 - 锁超时时间要略大于 DB 查询 + 写缓存的耗时,但别设太长(如超过 5 秒),否则等待线程积压严重
- 释放锁不能简单
DEL,得用 Lua 脚本保证“判断+删除”原子性,否则可能删掉别人刚加的锁
逻辑过期比物理过期更抗压,但要注意脏数据窗口
不设 EX,改在 value 里存一个逻辑过期时间戳,是另一种主流思路。它把“是否过期”的判断从 Redis 移到应用层,彻底避开物理过期那一瞬的并发洪峰。
但它不是银弹:新值还没刷进缓存前,所有请求拿到的都是旧数据。对价格、库存等强一致性场景不适用,但对商品描述、用户资料这类容忍延迟的场景很友好。
- 更新缓存必须加锁(仍用
SETNX),否则多个线程可能同时触发 DB 查询 - 异步刷新推荐走线程池或消息队列,别用
Thread.start(),避免线程爆炸 - 返回旧值时,要确保这个旧值本身没被其他逻辑误删或覆盖
- 监控逻辑过期 key 的刷新耗时,如果某次 DB 查询卡住,会导致后续所有请求都返回陈旧数据
预热 + 随机 TTL 是预防型手段,适合已知热点
如果能提前知道哪些是热点(比如首页 banner、秒杀商品、排行榜前 20),就别等它过期再救火,主动预热更稳。
但光预热不够——如果所有热点 key 都设成 1 小时过期,那整点一到就是雪崩前奏。必须打散过期时间。
- 预热时机选在低峰期(如凌晨 2 点),用脚本批量加载,避免和用户流量撞车
- TTL 建议加随机偏移,比如基础 3600 秒,再 ±300 秒:
3600 + random.randint(-300, 300) - 预热失败要告警,不能静默跳过;尤其注意预热用的 DB 查询是否走索引,别把预热变成慢查询源
- 动态热点(如突发新闻、热搜话题)没法靠预热覆盖,得配合前面两种实时防御手段
别忘了检查锁的释放时机和重试逻辑
很多线上事故不是锁没加,而是锁加了但没正确释放,或者重试没设上限,导致线程堆积、连接耗尽。
比如用递归重试 get_data(key),一旦 DB 挂了,所有线程会无限循环 sleep + 重试,最终把连接池撑爆。
- 重试最多 2–3 次,每次间隔递增(如 100ms → 200ms → 400ms),避免雪球效应
- 锁释放必须放在
finally块或使用try-with-resources,确保异常时也能清理 - 监控
lockkey 的平均存活时间和命中率,如果大量锁超时未释放,说明 DB 查询慢或锁粒度太粗 - 测试时一定要模拟锁冲突场景,光测单线程通过没意义










