缓存雪崩¶
💡 一句话概述
缓存雪崩是大面积 key 同时失效或缓存节点宕机,导致请求全部涌向 DB 持续高压——防护核心是分散失效时间和保障高可用。
🔑 核心概念¶
- 集中失效:大量 key 在同一时间点过期,或缓存节点故障导致缓存集体不可用。
- 持续高压:与击穿(瞬时尖峰)不同,雪崩是持续的 DB 过载。
- 级联故障:DB 压垮后,依赖 DB 的其他服务也开始超时,故障蔓延。
📝 详细说明¶
发生机制¶
graph TB
subgraph 雪崩["❄️ 缓存雪崩 — 大面积 key 集中失效"]
A1["大量 key 同时刻过期<br/>或缓存节点宕机"] --> A2["请求大面积 miss"]
A2 --> A3["全部涌向 DB"]
A3 --> A4["DB 持续高压 💥"]
A4 --> A5["级联故障<br/>依赖服务超时"]
end
两种触发方式:
| 触发原因 | 场景 |
|---|---|
| 大量 key 同时过期 | 批量导入数据时设了相同 TTL,凌晨 0 点大批 key 同时到期 |
| 缓存节点宕机 | Redis 主节点故障,未及时切换,所有请求穿透到 DB |
与击穿、穿透的区别¶
| 缓存击穿 | 缓存雪崩 | 缓存穿透 | |
|---|---|---|---|
| 根因 | 热 key 过期 | 大面积 key 同时过期 / 缓存宕机 | 查询不存在的数据 |
| 特征 | 单 key、高并发 | 多 key、集中失效 | 缓存永远 miss |
| 危害 | DB 瞬时尖峰 | DB 持续高压 + 级联故障 | DB 持续高压 |
🛡️ 解决方案¶
方案 1:随机失效(TTL Jitter)¶
给 TTL 加随机偏移量,避免大量 key 在同一时刻集中过期——最简单有效,批量设置 TTL 时必备。
graph LR
subgraph 无 Jitter["❌ 无随机偏移"]
T1["key_a TTL=30min"] --> E1["同时过期"]
T2["key_b TTL=30min"] --> E1
T3["key_c TTL=30min"] --> E1
E1 --> CRASH["DB 雪崩 💥"]
end
subgraph 有 Jitter["✅ 有随机偏移"]
J1["key_a TTL=30m+2m"] --> D1["32min 过期"]
J2["key_b TTL=30m+7m"] --> D2["37min 过期"]
J3["key_c TTL=30m+4m"] --> D3["34min 过期"]
D1 --> SAFE["DB 压力分散 ✅"]
D2 --> SAFE
D3 --> SAFE
end
无 Jitter -.->|加 jitter| 有 Jitter
func (s *Service) SetWithJitter(ctx context.Context, key string, data any, baseTTL time.Duration) {
jitter := time.Duration(rand.Intn(300)) * time.Second // 0~300s 随机偏移
s.rdb.Set(ctx, key, data, baseTTL+jitter)
}
// 批量设置时
for _, item := range items {
base := 30 * time.Minute
jitter := time.Duration(rand.Intn(600)) * time.Second
s.rdb.Set(ctx, item.Key, item.Data, base+jitter)
}
方案 2:加锁/限流¶
限制并发回源数量,避免 DB 过载。
// 使用带容量限制的令牌桶
var limiter = rate.NewLimiter(rate.Limit(100), 50)
func (s *Service) GetWithRateLimit(ctx context.Context, key string) (any, error) {
val, err := s.rdb.Get(ctx, key).Result()
if err == nil {
return val, nil
}
// 限流:超出速率直接拒绝或排队
if err := limiter.Wait(ctx); err != nil {
return nil, fmt.Errorf("service busy, try later")
}
data, err := s.db.Query(ctx, key)
if err != nil {
return nil, err
}
s.rdb.Set(ctx, key, data, 10*time.Minute)
return data, nil
}
方案 3:Redis 高可用 + 多级缓存¶
缓存节点宕机 = 所有 key 同时"失效",必须依赖高可用架构:
graph TB
subgraph 多级缓存架构
Client["客户端请求"]
L1["L1 本地缓存<br/>bigcache / ristretto"]
L2["L2 Redis<br/>Sentinel / Cluster"]
L3["L3 DB"]
Client --> L1
L1 -->|miss| L2
L2 -->|miss| L3
L3 --> L2
L2 --> L1
L1 -.->|Redis 不可用时<br/>降级到本地| Client
end
| 方案 | 说明 |
|---|---|
| Redis Sentinel | 自动故障转移,主节点挂掉时从节点升主 |
| Redis Cluster | 数据分片 + 自动故障转移,生产环境推荐 |
| 多级缓存 | 本地缓存(如 bigcache/ristretto)+ Redis,Redis 不可用时降级到本地 |
| 熔断降级 | DB 压力过大时直接返回兜底数据或错误页 |
// 多级缓存示例:本地 + Redis
func (s *Service) GetMultiLevel(ctx context.Context, key string) (any, error) {
// L1: 本地缓存
if v, ok := s.localCache.Get(key); ok {
return v, nil
}
// L2: Redis
val, err := s.rdb.Get(ctx, key).Result()
if err == nil {
s.localCache.Set(key, val, 5*time.Minute)
return val, nil
}
// L3: DB
data, err := s.db.Query(ctx, key)
if err != nil {
return nil, err
}
s.rdb.Set(ctx, key, data, 30*time.Minute)
s.localCache.Set(key, data, 5*time.Minute)
return data, nil
}
📊 方案对比¶
| 方案 | 防护方向 | 复杂度 | 适用场景 |
|---|---|---|---|
| 随机失效 | 集中过期 | ★ | 最简单有效,批量设 TTL 必备 |
| 加锁/限流 | DB 过载 | ★★★ | 应急限流,配合其他方案使用 |
| Redis 高可用 | 缓存宕机 | ★★★★ | 生产环境必备 |
| 多级缓存 | 缓存宕机 + DB 降级 | ★★★★ | 对可用性要求极高的核心链路 |
⚠️ 常见陷阱¶
批量导入数据时忘记加 jitter
最常见的雪崩根因。批量设缓存时统一用 SET key value EX 3600,3600 秒后所有 key 同时过期。
本地缓存与 Redis 缓存的一致性
多级缓存中,本地缓存 TTL 应远小于 Redis TTL,否则 Redis 更新后本地仍返回旧值。
Redis Cluster 全量迁移导致集中过期
扩容/缩容时 slot 迁移可能触发 key 重新设置 TTL,需要关注迁移过程中的 TTL 分布。
🏋️ 练习题¶
练习 1:为什么随机失效能防雪崩但不能防击穿?
随机失效分散的是**不同 key 的过期时间**。击穿针对的是**单个热点 key**,它只管一个 key 的过期,加不加 jitter 对单个 key 无意义。
答案
随机失效解决的是"多个 key 同时到期"的问题——让过期时间错开,避免集中回源。但击穿是单个 key 过期引发的并发回源,jitter 无法让同一个 key 的过期时间"分散"。防击穿需要加锁或逻辑过期。
练习 2:多级缓存中,如果本地缓存和 Redis 都 miss,大量请求同时打到 DB,怎么办?
本地缓存 + Redis 都 miss 说明是冷启动或 Redis 宕机场景。需要加限流或 singleflight 来控制回源并发。
答案
多级缓存只解决了"缓存层可用性"问题,没有解决"回源并发控制"。应该在 L3 回源前加一层限流(令牌桶)或 singleflight,确保 DB 不被瞬时请求压垮。生产环境通常是 多级缓存 + 限流 + 熔断 的组合。
🔗 相关链接¶
- 缓存击穿 — 单热点 key 过期的并发涌入
- 缓存穿透 — 查不存在的数据绕过缓存
- Redis 官方文档 — Caching — Redis 缓存模式