Redis缓存穿透、雪崩、击穿需区分应对:穿透用空值兜底;雪崩靠随机过期时间;击穿借SETNX分布式锁实现回源互斥,三者策略不可混用。

Redis 缓存穿透:查不到的 key 频繁打到 DB
缓存穿透本质是大量请求查询根本不存在的 key(比如恶意构造的 ID、已下线商品 ID),导致缓存不命中,所有请求穿透到数据库。DB 压力陡增,甚至被拖垮。
核心解法不是“拦住请求”,而是让缓存对“不存在”也给出明确响应——用空值或特殊标记兜底。
- 对
GET操作,查 DB 返回nil时,往 Redis 写入一个短过期的空值(如cache.Set(ctx, "user:999999", nil, 2*time.Minute)),注意值不能是 Go 的nil,得是可序列化的占位符(如"null"或自定义 struct) - 避免空值被误用:读取时需显式判断,比如反序列化后检查是否为
"null"字符串,再决定是否走 DB - 慎用布隆过滤器:它适合写多读少、key 空间稳定的场景;在用户 ID 类动态 key 下,重建成本高、存在误判,反而增加复杂度
- Go 客户端(如
github.com/go-redis/redis/v9)不自动处理空值逻辑,必须自己在业务层 wrapGet和Set
Redis 缓存雪崩:大量 key 同时过期压垮 DB
雪崩不是单个 key 失效,而是高并发下一批热点 key(比如首页商品列表、配置项)在同一秒过期,后续请求全量击穿到 DB。
关键不是“不让它们过期”,而是打破过期时间的强一致性集中点。
立即学习“go语言免费学习笔记(深入)”;
- 设置随机过期时间:基础 TTL 加上
rand.Int63n(600)秒扰动,例如time.Duration(baseTTL + rand.Int63n(600)) * time.Second - 避免用
EXPIRE单独设过期,优先在SET时一并指定(cache.Set(ctx, key, val, ttl)),防止 set 成功但 expire 失败的中间态 - 预热机制要谨慎:启动时批量加载可能触发 DB 尖峰;更稳妥的是按需懒加载 + 过期前异步刷新(见下一条)
- 如果用了
redis.NewClusterClient,注意各节点时间不同步可能导致实际过期偏差,建议统一 NTP 校时
缓存击穿:单个热点 key 过期瞬间的并发查询洪峰
和雪崩不同,击穿只针对一个 key(比如秒杀商品详情),过期那一刻多个请求同时发现缓存 miss,全部涌向 DB 查同一行。
本质是“原子性缺失”——需要确保只有一个请求去回源,其余等待结果。
- 用 Redis 的
SETNX(SetArgs{Mode: redis.SetNX})抢锁:抢到的 go 查询 DB 并写回缓存;没抢到的 sleep 后重试或用GET轮询 - 别用本地锁(如
sync.Mutex):在多实例部署下完全无效;分布式锁也别手写,直接用redis.Set(ctx, lockKey, reqID, redis.SetOptions{Expire: 10*time.Second, Mode: redis.SetNX}) - 锁的 value 必须唯一(如请求 traceID 或随机字符串),释放时用 Lua 脚本比对 value 再 del,防止误删其他请求的锁
- 超时时间要明显短于业务查询耗时(比如 DB 查询通常 50ms,锁设 300ms),否则等待线程堆积
Go 里用 redigo / go-redis 做缓存封装的坑
很多团队自己封装一层 cache interface,结果把穿透、雪崩逻辑耦合进工具包,反而让业务更难定制策略。
真正该封装的,是“怎么安全地读写”,而不是“怎么应对异常”。异常策略必须由业务决策。
-
go-redis/v9的Get方法返回redis.Nil错误表示 key 不存在,不是 Go 的nil;别用err == nil判断成功,要用errors.Is(err, redis.Nil) - 用
json.Marshal存结构体时,字段没加json:tag 会导致序列化为空对象;读取时若字段类型不匹配(如 int 存成 string),json.Unmarshal静默失败,值保持零值——务必加错误检查 - 连接池配置不当会放大问题:比如
MaxConnAge设太短,频繁新建连接;MinIdleConns为 0 时冷启动延迟高;建议MaxConnAge=30m、MinIdleConns=5 - 别在 defer 里调
client.Close():全局 client 生命周期应与应用一致,关闭时机错位会导致后续请求 panic 报"connection closed"
缓存的难点从来不在“怎么存”,而在“失效时谁来管、怎么管、管多久”。穿透、雪崩、击穿三者边界容易模糊,但应对方式必须严格对应触发场景——混用策略(比如给所有 miss 都塞空值)反而污染缓存语义。










