缓存穿透、击穿、雪崩背后的本质区别与治理方案
缓存穿透、击穿和雪崩都会把压力推向数据库,但根因不同。只背“布隆过滤器、互斥锁、随机 TTL”不够;必须先判断请求是否查询不存在数据、是否集中在一个热点,还是大量 Key 同时失效。
1. 三者的本质区别
| 问题 | 数据状态 | 请求形态 | 典型后果 |
|---|---|---|---|
| 穿透 | 缓存与数据库都不存在 | 大量不同或恶意 ID | 每次都访问数据库 |
| 击穿 | 热点数据存在但缓存失效 | 大量请求集中同一 Key | 瞬间并发重建 |
| 雪崩 | 大批缓存同时不可用 | 大范围请求回源 | 数据库整体过载 |
Redis 实例宕机也会造成类似雪崩,但它属于基础设施整体不可用,治理还需高可用、降级与隔离。
2. 穿透:拦截“永远查不到”
最简单的方式是缓存空值:
Product product = repository.findById(id);
if (product == null) {
redis.set(key, "__NULL__", SetArgs.Builder.ex(60));
return null;
}
redis.set(key, serialize(product), SetArgs.Builder.ex(1800));
空值 TTL 应短于正常数据,并区分“确实不存在”和“数据库临时失败”。若数据库超时也写空值,会把临时故障固化为错误结果。
海量随机非法 ID 可在入口校验格式、权限和取值范围,再用布隆过滤器判断“可能存在”。布隆过滤器会误判存在,但不会把已加入的数据判断为不存在;它不能替代数据库,也要处理新数据同步和重建。
还应限制单 IP、用户和接口速率。安全攻击不能只靠缓存层消化。
3. 击穿:只允许少量请求重建
热点 Key 到期的一瞬间,数千请求同时 miss。常见方案是互斥重建:
读缓存 miss
├─ 获得重建锁 → 查库 → 回填 → 释放锁
└─ 未获锁 → 短暂等待后重试,或返回旧值
锁必须有过期时间和唯一 token,防止进程崩溃后死锁与误释放。等待者也不能无限自旋,应有超时、抖动和降级。
另一种方案是逻辑过期:缓存值中保存业务过期时间,物理 Key 不立即删除。读到过期值时,仅一个线程异步刷新,其余请求暂时返回旧数据。它牺牲短时间强新鲜度换稳定性,适合商品详情、配置等可接受陈旧的数据,不适合余额和库存最终判断。
4. 雪崩:避免同步失效和同步回源
大量 Key 使用相同 TTL,批量导入后会在同一秒过期:
int ttl = 1800 + ThreadLocalRandom.current().nextInt(0, 300);
redis.set(key, value, SetArgs.Builder.ex(ttl));
随机 TTL 能打散时间,但不是全部方案。还需要:
- 多级缓存,本地缓存吸收部分流量;
- 数据库限流和连接池隔离,防止被拖死;
- 热点预热与主动刷新;
- 请求合并,减少相同查询;
- 熔断降级,返回旧值、默认值或友好错误;
- Redis Sentinel/Cluster 和客户端故障转移;
- 对恢复后的回源流量做渐进放量。
缓存恢复瞬间也可能产生“恢复风暴”,客户端不能无抖动地同步重试。
5. 通用读取模板
Optional<Product> get(long id) {
validate(id);
String key = "product:" + id;
String cached = redis.get(key);
if (cached != null) return decodeNullable(cached);
if (!bloom.mightContain(id)) return Optional.empty();
return rebuildWithSingleFlight(key, () -> repository.findById(id));
}
真实实现还要定义超时预算:Redis 读、锁等待、数据库查询和整体接口不能各自使用完整超时,否则层层叠加会超过调用方 deadline。
6. 不要忽略数据正确性
缓存是副本。空值缓存期间新建了数据怎么办?逻辑过期返回旧库存怎么办?治理策略必须与业务一致性等级配套:
- 新增成功后主动删除对应空值;
- 更新数据库后失效缓存;
- 关键写操作以数据库或专门库存系统为准;
- 响应中必要时标明数据时间;
- 缓存失败不能悄悄变成无限数据库回源。
7. 监控指标
- 命中率不能只看全局,要按业务和 Key 类型拆分;
- 空值命中数、布隆拒绝数;
- 单 Key QPS、重建耗时和锁等待者数量;
- 同一时段过期 Key 数量;
- 数据库回源率、连接池等待和拒绝数;
- 降级响应比例与旧值年龄。
8. 检查清单
- 不存在数据是否缓存空值并设置短 TTL?
- 热点重建是否合并请求且具备超时?
- TTL 是否加入随机抖动?
- Redis 故障时数据库是否有限流和隔离?
- 旧值策略是否符合业务一致性要求?
- 重试是否带退避、抖动和预算?
三类问题的共同原则是:不要让缓存 miss 无限制地等价于数据库查询。入口过滤、请求合并、失效打散、回源限流和业务降级应组成一条完整防线。