
为什么 singleflight 能拦住缓存击穿,但不是所有场景都该用它
缓存击穿本质是「大量并发请求同时打到一个失效的 key 上,穿透缓存直击下游(比如 DB)」。singleflight 的核心作用,是让同一时刻对同一个 key 的所有请求,只放行一个去执行真实加载逻辑,其余等待并共享结果——这确实能瞬间压平并发冲击。
但它不解决「key 长期失效」或「加载逻辑本身慢且失败率高」的问题;如果 Do 里调的是不稳定的 RPC 或 DB 查询,失败后所有等待者都会拿到错误,还可能把失败扩散成雪崩。所以它适合:加载逻辑快、成功率高、结果可缓存的场景(比如配置拉取、元数据查询)。
-
singleflight.Group是无状态的,每次 new 都是独立实例,别误以为全局复用能跨 goroutine 共享控制 - 它不自动续期缓存,也不管 TTL,和 Redis 或本地缓存要配合使用,不能替代缓存策略本身
- 如果 key 构造不一致(比如带了未标准化的时间戳、随机参数),
singleflight就完全失效——它只认字面量相等的key
Do 和 DoChan 怎么选:同步阻塞 vs 异步取消
Do 是最常用的方式,调用后当前 goroutine 阻塞,直到结果出来或出错;DoChan 返回一个 channel,适合需要超时控制、select 多路复用或主动取消的场景。
但要注意:DoChan 不会自动关闭 channel,必须手动读一次(哪怕只 select),否则 goroutine 泄漏;而 Do 在内部已做清理,更省心。
立即学习“go语言免费学习笔记(深入)”;
- 用
Do:简单加载、上下文超时由外层控制(比如 HTTP handler 用ctx传入) - 用
DoChan:需要在多个 channel 间 select,或想在结果返回前主动 abort(需配合context.WithCancel) - 无论哪种,
key类型必须可比较(不能是 slice、map、func),否则 panic:「panic: runtime error: comparing uncomparable type」
常见错误:重复加载、结果不一致、goroutine 泄漏
最典型的错误,是在 Do 的 fn 参数里又调了一次 Group.Do,形成嵌套调用,导致本应合并的请求被拆成多组;另一个高频坑是把指针或结构体地址当 key,看似不同变量,实则 key 相同(因为地址值一样),或者反过来,把内容相同但地址不同的切片当 key,导致无法去重。
- 永远用稳定、纯值的
key:推荐string,如"config:db_timeout";避免[]byte或struct{} - fn 函数体内不要再用同一个
Group去调Do,否则会绕过 singleflight 控制 - 如果 fn 返回 error,
singleflight不重试,也不会缓存 error 结果——这点常被忽略,下游仍可能反复触发失败路径
和 Redis pipeline / 本地缓存怎么配合才不翻车
singleflight 只负责「并发请求去重」,不负责「结果存储」。典型组合是:先查本地缓存(如 sync.Map)→ 没命中 → 进 Group.Do → 加载成功后写回本地缓存 + Redis。这里关键点在于「写缓存时机」——必须在 Do 的 fn 执行完、确认结果有效后再写,否则多个 goroutine 可能并发写,覆盖彼此。
- 不要在
Do外层查缓存失败就立刻写空值,那会把缓存穿透变成缓存污染 - Redis 写入建议用异步或延迟队列,别卡在
Do的 fn 里,否则拖慢整个 singleflight 响应 - 本地缓存建议用带 CAS 的操作(如
sync.Map.LoadOrStore),避免重复计算后又被后来者覆盖
真正难的从来不是加一行 Group.Do,而是 key 的定义是否收敛、加载函数是否幂等、错误路径是否可控——这些地方一松动,singleflight 就从保险丝变成导火索。










