直接并发查redis触发雪崩是因为大量请求同时发现缓存为空,穿透至后端批量重建,若未用singleflight合并请求且未兜底空值缓存,会导致数据库或接口瞬间过载。

为什么直接并发查Redis会触发雪崩
当大量请求同时发现缓存(GET user:123)为空,就会穿透到后端服务批量重建缓存。如果后端是数据库或HTTP接口,瞬间几百个相同 user:123 查询打过去,资源直接打满——这不是缓存没用,而是“空缓存没兜住 + 请求没合并”双重失效。
SingleFlight 不能直接包 Redis 的 Get 操作
singleflight.Group 的作用是:对相同 key 的多个并发调用,只让第一个真正执行函数,其余等待其返回结果。但它不感知 Redis 的语义,比如你传一个 redis.Client.Get(ctx, "user:123") 进去,它只会等这个 Get 完成,但不会帮你把 nil 结果写回 Redis,也不会自动加空值缓存防穿透。
常见错误写法:
// ❌ 错误:没处理 miss 场景,也没写缓存
v, err, _ := g.Do("user:123", func() (interface{}, error) {
return rdb.Get(ctx, "user:123").Result()
})
正确做法必须显式判断 redis.Nil 并决定是否回源、是否写空值:
- 先用
singleflight合并所有对同一 key 的请求 - 在 group 函数里调用
rdb.Get,若返回redis.Nil,则调用回源函数(如loadFromDB("user:123")) - 无论回源成功与否,都把结果(含空值)写入 Redis,并设 TTL
- 注意:空值建议用
SET user:123 "" EX 60而非nil,否则下次Get还是redis.Nil
如何避免 SingleFlight key 和 Redis key 不一致导致漏合并
典型坑:业务中 Redis key 是 "user:" + uid,但 singleflight.Do 传的是 "user_load_" + uid 或带参数哈希的字符串,结果看起来一样的请求被拆成多个 group,雪崩照旧。
必须保证两者 key 完全一致:
- 推荐统一用缓存 key 本身作为
singleflight的 key,例如"user:123" - 如果需区分读写场景(如更新时要 bypass group),再额外加前缀,但读场景保持纯净
- 避免对 key 做无意义转换:比如
fmt.Sprintf("%s_%d", "user", uid)和"user:" + strconv.Itoa(uid)表现可能不同(空格、符号),造成逻辑分裂
空值缓存 TTL 设置太短或太长都会出问题
写空值不是一劳永逸:SET user:123 "" EX 5 可能刚过期又来一波并发;设成 EX 3600 又会导致真实数据更新延迟暴露。
更稳妥的做法是分层控制:
- 空值 TTL 设为「基础保护值」,比如
EX 60,足够挡住突发流量 - 配合「逻辑过期」:实际存的 value 是 JSON,含
{"data":null,"expire_at":1717028400},应用层读取后自行判断是否需异步刷新 - 或者用「布隆过滤器」前置拦截非法 key,从源头减少
redis.Nil出现场景
SingleFlight 解决的是“同一合法 key 的并发击穿”,不是“海量非法 key 扫描”。后者得靠限流或过滤,别指望 group 拦得住。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











