缓存击穿、穿透、雪崩非redis bug,而是业务层未处理好set原子性、ttl扰动与空值序列化:击穿需用set+nx+ex防并发回源;穿透应缓存可识别空值(如"null")并设短ttl;雪崩须在set时随机化ttl打散过期时间。
缓存击穿、穿透、雪崩不是 redis 的 bug,而是业务代码里没处理好 set 原子性、ttl 扰动、空值序列化这三件事。go 客户端(比如 github.com/redis/go-redis/v9)不会替你兜底,必须自己在业务层写清楚逻辑。
用 SET + NX + EX 防击穿,别手写三步锁
击穿只针对单个热点 key(比如秒杀商品 ID),过期瞬间多个 goroutine 同时发现缓存 miss,全去查 DB —— 根因是“写缓存”没原子性。
- 错误做法:
if err == redis.Nil { user, _ := db.Get(id); cache.Set(ctx, key, user, ttl) },中间有竞态窗口,高并发下照样打穿 - 正确做法:用
cache.Set(ctx, key, val, redis.SetArgs{Mode: redis.SetNX, Expire: ttl})一步抢写;抢到的查 DB 并写最终值,没抢到的直接GET轮询或 sleep 后重试 - 锁过期时间要 ≥ DB 查询耗时(建议 ≥ 5s),但别超过 30s,否则故障时残留锁会拖垮后续请求
- 绝对别用
sync.Mutex或本地 map —— 多实例部署下完全无效
空值必须序列化为可识别的占位符,不能传 Go 的 nil
穿透本质是查不到的 key(如恶意 ID、已删商品)反复打 DB,解法不是拦请求,而是让缓存对“不存在”也返回明确响应。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
-
redis.Nil是客户端错误类型,不是业务空值;真正要缓存的是"null"字符串、或自定义 struct(如{Exists: false}) - 读取时必须显式判断:
if string(val) == "null" { return nil },否则会把空字符串当真实数据返回 - 空值 TTL 要短(如
2 * time.Minute),避免长期污染;也别设永久,误判后恢复成本高 - 布隆过滤器慎用:用户 ID 类动态 key 下重建成本高、存在误判,反而增加复杂度
设置随机 ttl 打散过期时间,别让一批 key 同一秒失效
雪崩不是 Redis 崩了,是大量热 key(如首页列表、配置项)在 EXPIRE 时间点高度重合,导致瞬间全量 miss。
- 错误写法:先
SET再单独调EXPIRE—— 网络失败会导致 key 永不过期,或只设成功一半 - 正确写法:在
SET时一并指定扰动后的 ttl,例如baseTTL + time.Second*time.Duration(rand.Int63n(600))(±10 分钟) - 别在
init()里调rand.Seed()—— Go 1.20+ 的math/rand全局 rand 默认安全;若需独立 seed,每次用rand.New(rand.NewSource(time.Now().UnixNano())) - 如果用了
redis.NewClusterClient,各节点系统时间偏差超几秒就可能让实际过期不一致,务必开 NTP 校时
三个问题的边界很清晰:穿透是“查根本不存在的 key”,击穿是“单个热点 key 过期瞬间并发回源”,雪崩是“一批 key 同时过期”。混用方案(比如给穿透加分布式锁)不仅无效,还会引入额外延迟和故障点。










