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)
Redis 挂了,本地缓存还能挡掉大部分流量(虽然命中率下降,但 DB 不崩)。
方案 3:热点 key 提前续期
后台任务监控热点 key(访问量 > 阈值),在过期前主动续期,让热点永不"自然过期"。
五、完整防御架构#
最后一道防线:DB 连接池 + 限流——就算缓存全挂,DB 也不能被直接打崩(返回降级提示优于 500)。
六、监控与演练#
三兄弟一句话记忆:
- 穿透:查不存在的,用布隆过滤器/空值缓存挡
- 击穿:查过期的热点,用互斥锁/逻辑过期挡
- 雪崩:大面积同时过期,用TTL 随机化/多级缓存挡
防御的本质:让 DB 永远只收到"必要且有限"的查询。缓存系统的设计目标不是"不崩",是"崩了 DB 也不死"。
微信公众号「福清而不淡」
后端架构 · AI 工程 · 云原生的一线实践,扫码关注,不错过更新。
本文已同步发布到公众号,欢迎留言交流。