singleflight 防击穿必须与缓存读取、db 查询、空值写入及布隆过滤器组成闭环;漏掉任一环(如do外查缓存、key不幂等、group未全局复用、loadfn内未写缓存)即失效。

SingleFlight 不能直接套在 Echo 的 handler 里就防击穿——它必须和缓存读取、布隆过滤器、DB 查询、空值写入组成闭环,否则只是把“100 次 DB 查询”变成“100 次排队等 1 次失败”。
为什么 Echo 中直接用 g.Do 还是击穿
常见错误是在 echo.Context 处理函数里先查 Redis,miss 后立刻调 g.Do(key, loadFn),但漏掉三个关键点:
- 没前置布隆过滤器,恶意 key(如
user:999999999)全进Do队列,触发 goroutine 阻塞放大 -
loadFn里只做db.QueryRow,没判sql.ErrNoRows,也没写空值缓存,下一轮请求照样击穿 -
key直接用c.Param("id"),没校验格式或标准化(比如带前导零、大小写混用),导致语义相同请求被当成不同 key
singleflight.Group 必须全局复用,别在 handler 里 new
Echo 的每个请求都走独立 goroutine,如果在 handler 里写 g := new(singleflight.Group),那每个请求都拿到全新实例,map[string]*call 全空,去重完全失效——压测时 DB QPS 依然线性上涨。
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
- ✅ 正确:声明为包级变量
var userGroup singleflight.Group,或注入到UserService结构体中 - ✅ 多个业务可共用一个 Group:
userGroup.Do和configGroup.Do不必拆,它本身线程安全且无状态 - ❌ 错误:在
func(c echo.Context) error内部定义g singleflight.Group,等于没加
loadFn 里必须完成「查 DB → 判空 → 写缓存」全链路
singleflight.Do 不存结果、不续缓存、不处理错误语义。它只管“谁来执行”,不管“执行完干啥”。所有副作用必须收进回调:
- DB 查询必须用
db.QueryRowContext(c.Request().Context(), ...),避免单个慢查询拖垮全部等待者 - 成功路径:扫描后调
cache.Set("user:"+id, u, ttl),确保下次请求能命中本地或 Redis 缓存 - 失败路径:仅当
err == sql.ErrNoRows时,才写空值缓存(如cache.Set("user:"+id, Null{}, 5*time.Minute));其他 err(如连接超时)不写,避免污染 - 别在
loadFn外层打日志或发 metric,否则竞态窗口里会重复记录
Key 设计要稳定、幂等、不含动态字段
Echo 的 c.Request().URL.String() 或 c.QueryParam("ts") 绝对不能进 key——时间戳、X-Request-ID、随机参数会让本该合并的请求彻底绕过 singleflight。
- ✅ 推荐构造方式:
"user:" + strings.TrimPrefix(c.Param("id"), "u_")(先清洗非法字符) - ✅ 强制类型转换:
"user:" + strconv.Itoa(userID),别传裸int,Do签名要求string - ❌ 危险示例:
fmt.Sprintf("%v", c.QueryParams())、c.Request().Host + c.Request().URL.Path - ⚠️ 注意:缓存 key、
Dokey、DB 查询条件三者必须严格一致,否则缓存写入和读取错位
最易被忽略的是:singleflight 从不自动写缓存,也不感知 TTL。你写进去的 key,如果没在 loadFn 成功路径里显式 cache.Set,那它就永远只是“一次性的合并”,而非“击穿防护”。










