缓存雪崩需用随机TTL打散过期时间,击穿需用singleflight或SETNX保证单次回源;二者必须分开处理,混用会增加延迟和故障风险。

缓存雪崩和击穿不是 Redis 的问题,是业务层没控制好过期时间扰动和回源原子性。雪崩靠随机 TTL 打散过期点,击穿靠 singleflight 或 SETNX 保证单次回源——两者必须分开处理,混用反而增加延迟和故障风险。
Go 中设置随机 TTL 防雪崩:别写死 time.Now().Add()
所有同类 key 用相同固定过期时间(比如统一 30 * time.Minute),服务重启或批量预热后就会扎堆失效。Redis 不会帮你加抖动,得自己算。
-
redis.Set(ctx, key, val, baseTTL + time.Second*time.Duration(rand.Int63n(600)))是安全写法;rand.Intn(600)在 Go 1.20+ 可直接用,不用手动rand.Seed() - 绝对不要先
SET再单独调EXPIRE—— 网络失败会导致 key 永不过期,或只设成功一半 - 如果用
redis.NewClusterClient,各节点系统时间差超过 2 秒就可能让实际过期不一致,必须开 NTP 校时 - 基础 TTL 别设太短(如
10s),否则抖动后部分 key 过期太快,起不到错峰作用;建议 ≥5 * time.Minute
singleflight 防击穿:必须闭环,不能只套 Do
singleflight.Group 本身不防击穿,它只合并请求。真正起作用的是「查缓存 → Do 回源 → 成功后写缓存」这个完整闭环。漏掉任一环,等于白加。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
-
var userGroup singleflight.Group必须声明为包级变量或注入 service 结构体,不能在 handler 里new(singleflight.Group)—— 否则每个请求都是新实例,Do完全无效 - 所有副作用操作(DB 查询、日志打点、指标上报、
cache.Set)必须全部塞进Do回调里,否则竞态窗口仍在 - 回调内必须用带
ctx的 API(如db.QueryRowContext(ctx, ...)),并自行处理超时与错误传播;失败时不写缓存,避免脏数据 - key 要稳定幂等,比如
"user:" + strconv.FormatInt(id, 10),不能含时间戳或随机数
SETNX 原子抢写防击穿:锁过期时间要卡准
适用于 Redis 单实例或对一致性要求极高的场景。比 singleflight 多一层跨进程保障,但要注意锁生命周期。
立即学习“go语言免费学习笔记(深入)”;
- 用
rdb.Set(ctx, key, placeholder, redis.SetArgs{Mode: redis.SetNX, Expire: 10 * time.Second})抢锁;抢到的查 DB 并Set最终值,没抢到的 sleep 后重试或轮询GET - 锁过期时间必须 ≥ DB 查询耗时(建议 ≥
5 * time.Second),但别超30 * time.Second,否则故障时残留锁拖垮后续请求 - 绝对别用
sync.Mutex或本地 map —— 多实例部署下完全无效 - 别手写三步锁(
GET→SETNX→DEL),网络中断或 panic 会导致锁残留;SetArgs是原子命令,更可靠
最易被忽略的一点:雪崩和击穿的触发边界非常清晰——雪崩是“一批 key 同时过期”,击穿是“单个热点 key 过期瞬间并发回源”。用同一个方案去套两者(比如给所有 key 加 singleflight),不仅没效果,还会把冷 key 的查询也串行化,白白增加 P99 延迟。

















