singleflight不是缓存替代品,而是缓存失效时防雪崩的安全阀;必须与缓存读写闭环配合,do需包裹整个加载逻辑,key须稳定幂等且与缓存key对齐,group须全局复用,缓存读取顺序影响一致性。

请求合并不是缓存的替代品,而是缓存失效时防止雪崩的“安全阀”;它必须和缓存读写闭环配合使用,否则毫无意义。
singleflight.Do 必须包裹整个加载逻辑,不能只包DB查询
常见错误是把 cache.Get 放在 g.Do 外面,导致两个 goroutine 都看到缓存 miss,然后各自进入 Do —— 虽然 DB 查询只执行一次,但日志、指标上报、租户校验、连接预初始化等副作用仍会重复发生。
- 所有带副作用的操作(
db.QueryRowContext、http.Do、cache.Set)必须全部塞进Do的回调函数里 - 回调内必须用带
ctx的 API,比如db.QueryRowContext(ctx, ...),不能用无超时的QueryRow - 失败时不写缓存,避免脏数据;成功后才调
cache.Set(key, val, ttl) - 返回的
shared布尔值只适合打点或降级判断,不能用来分支控制(比如“首次执行才发告警”)
Group 实例必须全局复用,不能 per-request new
singleflight.Group 内部靠 map 存 pending 请求,如果每次 HTTP handler 都 g := &singleflight.Group{},那每个实例的 map 都是空的,去重完全失效。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
- 正确做法是声明为包级变量:
var userGroup singleflight.Group - 或注入到 service 结构体中:
type UserService struct { group *singleflight.Group } - 多个业务(用户、配置、权限)可共用同一个
Group,无需拆分 - 别手动清理
Group状态,它不持有长期资源,也不需要Close
key 必须稳定、幂等,且与缓存 key 完全对齐
传 int、struct{} 或带时间戳的 URL 作为 key,会让本该合并的请求变成不同 key —— 比如 "user:123" 和 "user:123?t=1719784560" 在 singleflight 看来是两个独立请求。
- key 应该是业务语义上唯一标识资源的字符串,比如
"user:123"、"config:prod" - 避免拼接动态参数(如时间、随机数、traceID)
- 确保和缓存 key 一致:缓存写的是
"user:123",Do的 key 也必须是它
缓存读取顺序决定一致性,不是性能
三级缓存(本地 + Redis + DB)的读取顺序错了,会导致脏数据长期滞留。典型错误是「查 DB → 写本地 → 写 Redis」,一旦第二步失败,其他实例读 Redis 就拿到旧值。
- 正确顺序:先查本地缓存 → 未命中查 Redis → Redis 未命中再查 DB
- DB 查询成功后,并行写入本地缓存和 Redis(非串行),失败互不影响
- DB 返回结果必须带版本号或更新时间戳,写入 Redis 时作为 value 一部分,避免序列化差异导致结构不一致
- 本地缓存推荐用
ristretto,而非sync.Map:前者支持容量限制、LFU/LRU 混合淘汰、TTL,后者无淘汰策略,上线后容易 OOM
最易被忽略的点是:singleflight 只管“谁来执行”,不管“执行完怎么落库”。它和缓存策略是两层事——合并请求解决并发冲击,缓存闭环解决数据可见性。漏掉任一环,系统在高并发下就会不可控地退化。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










