跳过导航

缓存穿透、击穿、雪崩背后的本质区别与治理方案

约 5 分钟...次浏览
专栏Redis 深度专题第 2 篇

缓存穿透、击穿和雪崩都会把压力推向数据库,但根因不同。只背“布隆过滤器、互斥锁、随机 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 无限制地等价于数据库查询。入口过滤、请求合并、失效打散、回源限流和业务降级应组成一条完整防线。

分享:
文章作者:狼码纪
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。文章可能参考了其他优秀文章,如有侵权请联系删除。