缓存穿透、击穿、雪崩需分别应对:穿透用空值缓存+布隆过滤器;击穿用逻辑过期+互斥锁;雪崩靠过期时间随机化+二级缓存;三者均需监控与压测验证。

缓存穿透:查不到还一直查,null 不缓存是最大坑
缓存穿透本质是大量请求查询根本不存在的数据(比如恶意刷 id = -1 或已下线商品),每次都会穿透到 DB,压垮数据库。关键问题不在 Redis,而在于业务层没对空结果做合理缓存。
常见错误是只缓存「有值」的结果,遇到 null 或空集合直接跳过:
if (result != null) {
redisTemplate.opsForValue().set(key, result, 10, TimeUnit.MINUTES);
}这会导致后续相同请求反复打 DB。
- 正确做法:对确认不存在的 key,也缓存一个特殊占位符(如
"MISS")或空对象,并设较短过期时间(如 2 分钟) - 更稳妥方案:结合布隆过滤器(
BloomFilter)前置拦截——请求先过滤器,mightContain(id)返回false就直接拒绝,不查 Redis 也不查 DB - 注意布隆过滤器的误判率和容量预估,上线前用实际 ID 集合做一次
addAll()初始化,别靠运行时慢慢 put
缓存击穿:热点 key 过期瞬间,请求全涌向 DB
单个高频访问 key(如首页推荐列表)在过期时刻被大量并发请求同时发现失效,全部回源查 DB,造成瞬时压力。这不是数据不存在,而是「存在但刚好过期」。
单纯加锁(如 synchronized 或 RedisLock)能解决问题,但锁粒度和异常处理极易出错。
- 推荐用「逻辑过期」:value 存成
Map<String, Object>或自定义对象,包含真实数据 +expireTime字段;读取时先判断逻辑时间是否过期,过期则用互斥锁异步刷新,未过期直接返回 - 避免用
SETNX + EXPIRE两步操作——存在竞态:设置成功但过期失败,key 变成永不过期 - 如果必须用分布式锁,确保锁 key 带唯一随机值(如
UUID),释放时用 Lua 脚本比对再删,防止误删别人持有的锁
缓存雪崩:大批 key 同一时刻过期,DB 直接卡死
雪崩和击穿的区别在于规模——不是单个 key,而是多个甚至全部热点 key 的过期时间高度集中(例如批量写入时都设了 expire = 30 * 60 秒),导致整点流量高峰时集体失效。
立即学习“Java免费学习笔记(深入)”;
最典型诱因是代码里硬编码固定过期时间:
redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);
- 解决核心:打散过期时间。基础过期时间 + 随机偏移量(如
30 * 60 + ThreadLocalRandom.current().nextInt(600)),让过期分布更平滑 - 二级缓存可作为兜底:本地 Caffeine 缓存 + Redis 组合,即使 Redis 全挂,本地仍能扛住部分读请求
- 服务启动时主动预热:加载核心 key 到 Redis 并设不同过期时间,避免冷启动后第一波请求全击穿
三者共性陷阱:没监控就等于没防护
很多团队加了布隆过滤器、逻辑过期、随机 TTL,但线上依然出问题——因为没看真实指标。
- 必须监控 Redis 的
keyspace_misses(穿透)、expired_keys(雪崩倾向)、慢日志中大量GET后紧跟SET(击穿信号) - DB 层要埋点统计「缓存未命中后成功查到数据」的请求量,持续高于阈值就得告警
- 测试阶段用
jmeter模拟穿透请求(ID 递增+随机负数)或定时触发过期(EXPIREAT key $(date +%s)),验证防护逻辑是否真生效
缓存问题从来不是“加个功能”就结束的事,真正难的是把防护逻辑嵌进业务流程、配上可观测性、并接受它在极端场景下仍可能漏掉几个请求——这时候降级开关和 DB 限流才是最后防线。

















