4. 缓存雪崩:大规模缓存同时失效,多级缓存架构与降级策略

缓存雪崩,说白了就是一大片缓存同时挂了。

不是单个key失效,是成百上千个key,在同一时刻集体罢工。这时候所有请求直接穿透到数据库,你想想看,数据库扛得住吗?

我在项目中遇到过这种场景。有一次大促活动,零点刚过,系统瞬间被打爆。排查下来,不是代码bug,是缓存同时过期了。嗯,那感觉,真是刻骨铭心。

4.1 缓存雪崩的本质

缓存雪崩和缓存穿透不一样。穿透是单个key没命中,雪崩是大面积key同时失效。

为什么会这样?

最常见的原因就两个:

  • 缓存同时过期:比如你给一批商品设置了相同的过期时间,比如凌晨0点过期。那到了0点,这批缓存全部失效,请求全打到数据库。
  • 缓存节点宕机:比如Redis集群某个分片挂了,那这个分片上的所有缓存都不可用。如果流量大,其他节点也可能被拖垮。

核心问题:缓存雪崩的本质是「保护层失效」,导致底层系统承受远超预期的压力。

4.2 多级缓存架构

我个人习惯用多级缓存来对抗雪崩。说白了,就是给系统多穿几层防弹衣。

典型的架构是这样的:

客户端 → CDN缓存 → 本地缓存(Caffeine) → 分布式缓存(Redis) → 数据库

每一层都承担一部分流量。上层扛不住,还有下层兜底。

我画了一张图,你看一下这个流程:

多级缓存架构流程图 CDN 缓存 命中率约 60%-80% 本地缓存 Caffeine / Guava Redis 缓存 分布式缓存 数据库 MySQL / 其他 降级策略 缓存失效 → 返回默认值 或限流排队 请求从上到下逐层穿透,每一层都是保护屏障 上层命中则直接返回,不继续向下

4.3 缓存雪崩的预防策略

预防永远比事后补救划算。我总结了几个实战中验证过的策略:

4.3.1 过期时间打散

别把所有key的过期时间设成一样的。加个随机偏移量。

// 错误做法:所有key同时过期
redis.set(key, value, 3600);

// 正确做法:基础时间 + 随机偏移
int baseExpire = 3600;
int randomOffset = new Random().nextInt(600); // 0-600秒随机
redis.set(key, value, baseExpire + randomOffset);

我曾经在一个项目中,把所有商品缓存都设成了24小时过期。结果每天凌晨3点,数据库准时被打爆。后来加了随机偏移,问题就解决了。

4.3.2 缓存预热 + 互斥锁

对于热点数据,提前加载到缓存。同时用互斥锁防止并发重建缓存。

// 缓存重建时加锁
String lockKey = "lock:" + key;
boolean locked = redis.setNx(lockKey, "1", 5); // 5秒超时
if (locked) {
    try {
        // 从数据库加载数据
        Object data = db.query(key);
        redis.set(key, data, expireTime);
    } finally {
        redis.del(lockKey);
    }
} else {
    // 没拿到锁,等待后重试
    Thread.sleep(50);
    return redis.get(key); // 可能拿到旧数据
}

小技巧:互斥锁的粒度要细。别锁整个缓存,只锁当前key。否则性能会下降得很厉害。

4.3.3 二级缓存永不过期

对于核心数据,比如用户信息、商品详情,我建议用二级缓存。一级缓存有过期时间,二级缓存永不过期。

一级缓存失效了,从二级缓存拿数据。同时异步去数据库刷新一级缓存。

// 一级缓存:短过期
Object data = redis.get(key);
if (data == null) {
    // 从二级缓存拿
    data = localCache.get(key);
    if (data != null) {
        // 异步刷新一级缓存
        executor.submit(() -> {
            Object freshData = db.query(key);
            redis.set(key, freshData, 300);
        });
    }
}

4.4 降级策略

缓存雪崩发生了怎么办?别慌,降级。

降级不是不处理,是「有损服务」。保证核心功能可用,非核心功能暂时关闭。

降级级别 触发条件 处理方式 影响范围
一级降级 缓存命中率 < 50% 返回默认值/静态数据 部分数据不实时
二级降级 数据库QPS > 阈值 限流 + 排队 部分请求延迟
三级降级 数据库连接池耗尽 直接返回失败提示 部分请求失败

注意:降级一定要有「熔断恢复」机制。别降级了就回不来了。我见过一个系统,降级后忘了恢复,结果跑了三天「降级模式」,用户投诉一大堆。

4.5 实战避坑指南

最后分享几个我踩过的坑:

  • 别依赖单一缓存:Redis再稳也有宕机的时候。本地缓存 + Redis 双保险。
  • 监控缓存命中率:命中率突然下降,就是雪崩的前兆。我习惯在监控面板上放这个指标。
  • 数据库连接池要限流:缓存雪崩时,数据库是最后一道防线。连接池设个上限,别让数据库被打死。
  • 降级开关要手动+自动:自动降级可能误判,手动降级又太慢。两者结合最稳妥。

我曾经在一个金融交易系统里,就因为没做缓存降级,导致数据库连接池被打满,整个交易停了5分钟。嗯,从那以后,我再也不敢忽视缓存雪崩了。

总结一句话:缓存雪崩不可怕,可怕的是没有预案。多级缓存 + 过期时间打散 + 降级策略,这三板斧用好了,系统稳如老狗。


公众号:蓝海资料掘金营,微信deep3321