缓存击穿本质是热点key过期瞬间的并发竞争,仅qps高、重建耗时长且ttl同时到期的热点key(如秒杀商品)会触发;典型特征为get返回null后直接查库,解决方案以互斥锁(setifabsent带过期时间+二次检查+lua安全删锁)和逻辑过期(永不过期+业务层判断+异步刷新)为主。

缓存击穿的本质是热点 key 过期瞬间的并发竞争
不是所有 key 失效都会引发击穿,只有那些 QPS 高、重建耗时长、且 TTL 刚好同时到期的热点 key 才会出问题。比如秒杀商品详情页、热搜榜单 top10 的缓存,一旦过期,几十甚至上百请求几乎同时发现缓存为空,全部涌向数据库。
关键判断点:redisTemplate.opsForValue().get(key) 返回 null 后,是否直接调用 userMapper.selectById(id) —— 如果是,就存在击穿风险。
互斥锁方案:用 setIfAbsent 控制重建入口
这是最常用也最稳妥的方案,核心是让只有一个线程去查库+写缓存,其他线程等待或重试。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
-
setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS)必须带过期时间,否则锁可能永久残留 - 加锁后要再次检查缓存(防止其他线程已写入),再查库,避免重复加载
- 释放锁必须放在
finally块里,但注意:不要用redisTemplate.delete(lockKey)直接删——可能删掉别人刚续上的锁;更安全的做法是用 Lua 脚本原子删锁,或改用 Redisson 的RLock - 重试逻辑别用无限递归,建议限制最大重试次数(如 3 次)+ 指数退避,避免线程栈溢出或雪崩式等待
逻辑过期方案:把过期判断从 Redis 移到业务层
不依赖 Redis 的 TTL 机制,而是把“过期时间”作为字段存进缓存值里(比如 JSON 中加 expireTime 字段)。Redis 本身设为永不过期,由代码控制是否触发异步重建。
- 优点:无锁,高并发下响应更快;避免锁等待导致的线程阻塞
- 缺点:缓存值永远存在,内存占用略高;需额外维护异步任务(如用
ThreadPoolTaskExecutor),失败时要兜底重试 - 典型误用:
redisTemplate.opsForValue().set(key, value)没存expireTime字段,或读取后没校验时间戳就直接返回,等于没生效 - 重建时机:检测到逻辑过期后,应立即返回旧值 + 异步刷新,而不是阻塞等待新值
容易被忽略的细节:锁粒度、key 设计和监控
击穿防护不是加了锁就万事大吉。生产环境里真正踩坑的地方往往藏在细节里:
- 锁 key 必须和业务 key 强绑定,比如
"lock:user:" + id,不能写成固定字符串"lock:user",否则所有用户共用一把锁 - 不要对整个 service 方法加锁,只锁具体 key 的重建过程;否则
getUserById(1)和getUserById(2)会互相阻塞 - 上线后必须监控
lockKey的命中率和平均等待时长,如果大量请求卡在Thread.sleep(50),说明锁竞争严重,可能需要调大锁超时或优化重建逻辑 - Redis 自身的
set命令响应慢(比如网络延迟或主从同步延迟),会导致锁获取失败率上升,这时得结合tryLock的重试参数做适应性调整
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










