Redis 缓存穿透 / 击穿 / 雪崩:三兄弟的完整防御

“缓存穿透、击穿、雪崩"是后端面试必考题,也是线上事故高发区。三个名字像,机制完全不同,防御手段也不同。这篇文章把三者讲透 + 给完整防御方案。

一、三兄弟的区别(先分清)

穿透 击穿 雪崩
对象 不存在的 key 一个热点 key 大量 key
现象 请求直接打到 DB 缓存过期瞬间并发打 DB 大面积缓存同时过期
危害 DB 被无效查询打爆 单点 DB 压力尖峰 DB 整体被打爆

二、缓存穿透:查的东西不存在

现象:恶意或异常请求查询不存在的 ID(比如 user_id=-1、随机不存在的简历 ID),缓存查不到 → 每次都打 DB。

防御

方案 1:空值缓存(最简单)

1
2
3
4
5
6
7
8
9
// 查不到就缓存空值,短期防止重复打库
val, err := redis.Get(ctx, key)
if err == redis.Nil {
    data := db.Get(id)
    if data == nil {
        redis.Set(ctx, key, "NULL", 60*time.Second)  // 空值也缓存,60s
        return nil, nil
    }
}

方案 2:布隆过滤器(更优)

1
2
3
4
5
// 数据存在性预判:不存在直接拒绝,不进缓存层
bloom := bloomFilter // 初始化时把全量 ID 灌进去
if !bloom.Exists(id) {
    return nil   // 一定不存在,直接返回,连 Redis 都不查
}

对比:空值缓存简单但有"无效 key 占内存"的代价;布隆过滤器内存极省(10 亿 ID 几 GB),但有误判率(会放过少量不存在的)。组合:布隆过滤器在前,空值缓存兜底。

三、缓存击穿:一个热点 key 过期

现象:某个超高热度的 key(比如首页热门职位榜)缓存过期瞬间,几千请求同时发现缓存为空,全部打到 DB

防御

方案 1:互斥锁(最常用)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func getHot(key string) string {
    if val := redis.Get(key); val != nil {
        return val
    }
    // 缓存没有 → 加锁,只让一个请求去重建缓存
    lock := redis.SetNX("lock:"+key, token, 10*time.Second)
    if lock {
        defer redis.Del("lock:" + key)
        val := db.Get(key)          // 只有拿锁的请求查 DB
        redis.Set(key, val, ttl)
        return val
    }
    // 没拿到锁:短暂等待后重试,或返回旧值(如果双缓存)
    time.Sleep(50 * time.Millisecond)
    return getHot(key)   // 递归重试(要限次数)
}

方案 2:逻辑过期(缓存永不过期)

1
2
3
4
5
6
7
// 缓存不设 TTL,value 里带逻辑过期时间
val = json{Data: data, ExpireAt: now + 30*60}
// 读取时发现逻辑过期 → 异步重建(返回旧值给当前请求)
if val.ExpireAt < now {
    go rebuildCache(key)   // 后台重建,当前请求先拿旧值
    return val.Data
}

互斥锁 vs 逻辑过期:互斥锁简单但击穿瞬间有等待;逻辑过期无等待(返回旧值)但要处理"旧值过期窗口”,且数据短暂不一致。追求一致性用锁,追求响应用逻辑过期。

四、缓存雪崩:大面积 key 同时过期

现象:缓存设置了统一的 TTL(比如全部 30 分钟),同一批 key 同时过期;或 Redis 整个挂了。请求全部打到 DB → DB 崩。

防御

方案 1:过期时间加随机值(最有效)

1
2
3
// 不要让 key 同生共死:TTL = 基准 + 随机抖动
ttl := 30*60 + rand.Intn(5*60)   // 30分钟 ± 5分钟
redis.Set(ctx, key, val, ttl)

方案 2:多级缓存(本地 + Redis)

R D e B d C i a s f f e i n e / L R U

Redis 挂了,本地缓存还能挡掉大部分流量(虽然命中率下降,但 DB 不崩)。

方案 3:热点 key 提前续期

后台任务监控热点 key(访问量 > 阈值),在过期前主动续期,让热点永不"自然过期"。

五、完整防御架构

R D e B d i s + T T k L e y 穿 D / B 穿

最后一道防线:DB 连接池 + 限流——就算缓存全挂,DB 也不能被直接打崩(返回降级提示优于 500)。

六、监控与演练

D B " < R e 8 d 0 i = % s " D B Q P S R e d i s

总结

三兄弟一句话记忆:

  • 穿透:查不存在的,用布隆过滤器/空值缓存
  • 击穿:查过期的热点,用互斥锁/逻辑过期
  • 雪崩大面积同时过期,用TTL 随机化/多级缓存

防御的本质:让 DB 永远只收到"必要且有限"的查询。缓存系统的设计目标不是"不崩",是"崩了 DB 也不死"。