缓存击穿是指热点key过期瞬间大量并发请求穿透缓存直击数据库的现象;其本质是单机锁无效、需用带过期时间与业务前缀的setnx分布式锁,并严格校验空值、续期锁、设置合理缓存过期时间。

缓存击穿到底是什么现象
缓存击穿不是“数据不存在”,而是「一个本该存在的热点 key 突然过期,紧接着大量并发请求同时发现缓存 miss,一股脑涌向数据库」。典型场景比如秒杀商品详情页、首页 banner 轮播图、活动倒计时配置——这些 key 本身有值,但过期时间一到,瞬间被清空,Redis 拦不住流量,DB 直接被打满。
为什么不能只靠 synchronized 或 volatile 变量
Java 层的 synchronized 只在单机 JVM 内有效,集群部署下完全失效;volatile 更是连原子性都不保证。你写个 if (value == null) { synchronized(...) { ... } },在多实例服务里,每个节点都会各自去查 DB,击穿照旧发生。
- 本地锁对跨进程/跨机器请求毫无约束力
-
volatile不提供锁语义,无法阻塞其他线程重试 - 即使单机压测不爆,上线后集群环境必然重现问题
用 SETNX 实现分布式互斥锁才是正解
核心思路:让 Redis 自己当裁判——谁先成功执行 SETNX mutex:goods:1001 1,谁就获得加载权限;其他人轮询等待,直到锁释放或超时。注意这不是“加完锁再查缓存”,而是「查缓存 → miss → 尝试抢锁 → 成功则查库回填 → 失败则休眠重试」。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
-
SETNX必须配EX设置过期时间(如SETNX mutex:goods:1001 1 EX 30),否则锁崩溃后永不释放 - 锁 key 名要带业务前缀(如
mutex:goods:1001),避免不同资源误删彼此的锁 - 释放锁必须用 Lua 脚本原子判断 key 存在且值匹配,防止 A 加锁、B 误删、A 回填失败导致脏数据
- 客户端休眠时间别设太短(如
Thread.sleep(50)),否则重试风暴反而加重 Redis 压力
实际代码里最容易漏掉的三件事
很多团队写了 SETNX 却还是翻车,问题不在逻辑,而在边界处理。
- 没校验
GET返回的是否真是业务空值(比如"null"字符串、空 JSON"{}"、甚至"0")——建议统一用Boolean.FALSE或自定义占位对象标识“查无此数据” - 锁超时时间(如 30s)远大于 DB 查询耗时(如 200ms),但没做「续期」或「异步兜底」,结果锁提前过期,多个线程又同时进 DB
- 回填缓存时没设置合理过期时间,比如直接
SET goods:1001 {...} EX 86400,下次更新全靠人工踢缓存,极易造成脏读
真正稳定的方案,往往不是锁本身多精巧,而是对「空值判定」「锁生命周期」「缓存写入一致性」这三点反复校验。尤其在灰度发布或 DB 主从延迟场景下,漏掉任意一环,击穿就会换个姿势回来。










