缓存穿透、击穿、雪崩本质均为请求绕过或压垮缓存直冲数据库,需结合业务场景、缓存策略与兜底机制协同解决:穿透须缓存空值或布隆过滤器拦截;击穿应错开过期时间或加互斥锁;雪崩重在分散ttl并引入熔断降级。

缓存穿透、击穿、雪崩不是 Redis 独有问题,但 Redis 作为主流缓存组件,暴露得最直接;三者本质都是「请求绕过或压垮缓存,直冲数据库」,解决思路必须结合业务场景+缓存策略+兜底机制,不能只靠加一层 if (cache == null)。
缓存穿透:查不到还疯狂查
典型现象:get(key) 返回 null,但大量请求携带**根本不存在的 key**(比如恶意构造的 ID、爬虫扫接口),导致每次请求都穿透到 DB,DB 压力陡增。
- 根本原因不是 Redis 慢,而是「空结果没缓存」+「无校验机制」
- 常见误操作:查 DB 返回
null后,直接返回,不写缓存 - 正确做法是:对确认「业务上不可能存在」的 key(如负数 ID、超长字符串),写入一个短 TTL 的空值(如
setex key 60 "null"),让后续请求先命中缓存再拒绝 - 更彻底方案是用布隆过滤器(Bloom Filter)前置拦截——但注意:布隆过滤器有误判率,且需和 DB 数据一致;更新 DB 时必须同步更新过滤器,否则出现「漏放」
缓存击穿:热点 key 过期瞬间崩盘
典型现象:某个高热度 key(如首页 banner 配置、秒杀商品)设置了过期时间,到期瞬间大量并发请求同时发现缓存失效,全部打到 DB,DB 瞬间被打满。
Java JDK 25 来自 OpenJDK 官方归档,版本为 JDK 25,本条下载地址已指向官方 Windows x64 zip 安装包直链,适合调试旧项目或兼容旧版 Java 运行环境。
- 和穿透不同,击穿的 key 是真实存在的,只是「过期时间点」成了单点故障
- 避免方式不是取消过期,而是错开或消除「集中过期」:比如设置随机 TTL(
600 + random(100)秒),或用逻辑过期(value 中自带时间戳,过期不删 key,由应用判断) - 更稳妥的是加互斥锁:第一个发现缓存为空的线程去查 DB 并重建缓存,其余线程等待或重试;但锁粒度要细(按 key 分片),且必须设超时(防死锁),推荐用 Redis 的
set key value ex 30 nx实现分布式锁
缓存雪崩:大面积 key 同时失效
典型现象:Redis 中大量 key 设置了相近的过期时间(比如批量预热时统一设了 2 小时 TTL),到期后集体失效,流量洪峰瞬间压垮 DB。
- 比击穿更广域,影响范围是「一批 key」而非单个
- 预防重点在「分散过期时间」:写入时主动增加随机偏移(如
expireTime = base + ThreadLocalRandom.current().nextInt(1000)) - 架构层要加熔断和降级:当 DB 调用失败率超过阈值(如 50%),自动 fallback 到默认值或本地缓存(
Caffeine),而不是继续重试 - 不要依赖单一 Redis 实例:主从 + 多副本 + 读写分离能缓解单点压力,但无法解决雪崩根源——过期设计不合理
Java 实操中容易被忽略的细节
很多人写了空值缓存、加了锁、分散了 TTL,线上还是出问题——往往卡在这些地方:
- Spring Cache 的
@Cacheable默认不缓存null,必须显式配置unless="#result == null"或改用CacheManager手动控制 - 用
RedisTemplate写空值时,如果序列化器是GenericJackson2JsonRedisSerializer,null会序列化成 JSON 字符串"null",而非真正的空值,导致判断失效 - 布隆过滤器加载时机错误:在应用启动时从 DB 全量加载,但 DB 数据变更后过滤器未更新,变成“假阴性”——建议用 Canal 监听 binlog 增量同步
- 锁超时时间必须小于业务查询 DB 的最大耗时,否则可能前一个线程还没写完缓存,锁就释放了,后一个线程又去查 DB,造成重复加载
String key = "user:1001";
String cacheValue = redisTemplate.opsForValue().get(key);
if (cacheValue != null) {
return cacheValue;
}
// 尝试获取分布式锁
String lockKey = "lock:" + key;
Boolean isLocked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", Duration.ofSeconds(3));
if (Boolean.TRUE.equals(isLocked)) {
try {
String dbValue = queryFromDatabase(key); // 真实 DB 查询
if (dbValue != null) {
redisTemplate.opsForValue().set(key, dbValue, Duration.ofMinutes(10));
} else {
// 注意:这里写空值,且 TTL 要短(如 2 分钟),避免长期占内存
redisTemplate.opsForValue().set(key, "", Duration.ofSeconds(120));
}
return dbValue;
} finally {
redisTemplate.delete(lockKey);
}
} else {
// 等待 50ms 后重试,避免忙等
Thread.sleep(50);
return getWithLock(key);
}
真正难的不是写出这几十行代码,而是想清楚:这个 key 是否真的需要缓存?它的生命周期谁来管理?DB 挂了怎么办?监控告警有没有覆盖空值命中率、缓存 miss ratio、锁等待时长?这些才是面试官想听到的“背后思考”。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










