前言
Redis 缓存作为高性能的数据访问层,在实际开发中经常面临三大经典问题:缓存击穿、缓存穿透、缓存雪崩。
本文将从它们各自的定义、产生的原因、实际开发过程中的解决方案出发,为大家详细描述相关的信息,并附有相关的go代码示例(嗯…最近go写的比较多,大家也可以用其它语言带入,原理都是一样的)
🧨 一、缓存穿透(Cache Penetration)
❓是什么?
- 客户端频繁请求数据库中根本不存在的 Key,缓存不命中、数据库也查询不到,导致所有请求都打到数据库,形成“穿透”。
⚠️产生原因:
-
恶意攻击者大量请求随机 key
-
参数异常或用户请求错误
✅解决方案
1. 设置空值缓存(推荐)
// 查询用户信息,缓存穿透处理:不存在也缓存
func GetUserInfo(id string) (string, error) {
key := "user:" + id
val, err := redisClient.Get(ctx, key).Result()
if err == redis.Nil {
// 缓存未命中,查数据库
dbVal := queryDB(id)
if dbVal == "" {
// 防止穿透:缓存空值,设置短过期
redisClient.Set(ctx, key, "null", 30*time.Second)
return "", nil
}
redisClient.Set(ctx, key, dbVal, time.Hour)
return dbVal, nil
}
if val == "null" {
// 命中空值缓存
return "", nil
}
return val, nil
}
2. 使用布隆过滤器(高并发下更优)
-
将可能存在的 key 预先存入布隆过滤器
-
只有布隆判断为可能存在的,才访问 Redis / DB
可选库:github.com/bits-and-blooms/bloom/v3
var bf = bloom.NewWithEstimates(1000000, 0.01)
func InitBloomFromDB() {
for _, id := range queryAllIDs() {
bf.Add([]byte(id))
}
}
func CheckWithBloom(id string) bool {
return bf.Test([]byte(id))
}
💥 二、缓存击穿(Cache Breakdown)
❓是什么?
- 某个热点 key 失效的一瞬间,大量请求同时击穿缓存打到数据库,瞬间压垮 DB。
⚠️产生原因:
-
热点数据失效,瞬时并发请求全部走数据库
-
缓存更新不及时
✅解决方案
1. 设置互斥锁(分布式锁)
每次缓存未命中时,只有一个线程能去查数据库,其他等待。
func GetProductDetail(id string) string {
key := "product:" + id
val, err := redisClient.Get(ctx, key).Result()
if err == redis.Nil {
lockKey := "lock:" + id
ok, _ := redisClient.SetNX(ctx, lockKey, "1", 10*time.Second).Result()
if ok {
defer redisClient.Del(ctx, lockKey)
val = queryDB(id)
redisClient.Set(ctx, key, val, 5*time.Minute)
} else {
// 等待缓存刷新后再取
time.Sleep(100 * time.Millisecond)
return GetProductDetail(id)
}
}
return val
}
2. 提前续期:热点 key 永不过期
-
定时任务刷新 key 的 TTL,保持热 key 存在
-
或缓存永不过期,仅通过后台异步更新值
3. 多级缓存:本地缓存 + Redis
- 使用 Go 的 sync.Map 或 bigcache 等组件构建一级缓存
❄️ 三、缓存雪崩(Cache Avalanche)
❓是什么?
- 大量缓存同时过期或 Redis 故障,导致请求瞬间全部打到数据库,造成系统雪崩。
⚠️产生原因:
- 统一设置了大量 key 的相同过期时间
- Redis 整体不可用或宕机
✅解决方案
1. 过期时间加随机
- 避免所有缓存同一时间失效
ttl := 60 + rand.Intn(30) // 60~90秒
redisClient.Set(ctx, key, val, time.Duration(ttl)*time.Second)
2. 构建本地缓存备份(降级缓存)
var localCache = sync.Map{}
func GetWithFallback(id string) string {
key := "data:" + id
val, err := redisClient.Get(ctx, key).Result()
if err != nil {
if v, ok := localCache.Load(key); ok {
return v.(string)
}
val = queryDB(id)
redisClient.Set(ctx, key, val, time.Minute)
localCache.Store(key, val)
}
return val
}
3. 熔断降级 + 限流保护 (golang中gin+redis实现熔断与限流)
- 高并发或 DB 延迟高时,短路请求或返回默认值,避免连锁反应
✅ 1. 熔断机制(Circuit Breaker)
📌 核心思想:
-
监控数据库或下游服务的错误率和响应时间
-
如果连续失败或延迟严重,则“熔断”请求,快速失败,防止压垮系统
-
一段时间后尝试恢复(Half-Open 状态)
✅ 用处:
断开数据库压力路径,即便缓存失效也避免瞬时打爆 DB
✅ 2. 限流机制(Rate Limiting)
📌 核心思想:
-
限制每秒(或每分钟)访问次数,控制系统负载
-
超出请求数的用户请求被拒绝、排队或延迟
✅ 用处:
- 限制缓存失效后的并发量,避免同时大量穿透到数据库
🔚 总结对比表
问题类型 | 现象 | 原因 | 核心解决方案 |
---|---|---|---|
穿透 | 缓存&DB都没数据 | 攻击/异常ID | 空值缓存 / 布隆过滤器 |
击穿 | 热点 key 同时失效 | 高并发打穿缓存 | 加锁 / 永久缓存 / 本地缓存 |
雪崩 | 大面积缓存同时失效 | 集中过期 / Redis挂 | 随机过期 / 降级缓存 / 限流熔断 |