不能直接用 goroutine + channel 做请求合并,因其无法消除重复请求、易触发限流/超时,且缺乏并发控制与等待机制;应优先使用 singleflight 处理相同 key 的并发请求,或自研双触发 batcher 实现按时间/数量聚合。

为什么不能直接用 goroutine + channel 做请求合并
直接起一堆 goroutine 发请求,看似高并发,实则容易触发服务端限流、连接耗尽或下游超时。更关键的是——它根本不“合并”。真正需要合并的场景是:多个协程几乎同时请求同一资源(比如 GetUser(123)),你得把它们攒成一次调用,而不是发 10 次重复请求。
典型错误是只用 sync.Map 缓存结果,却没控制并发写入和等待逻辑,导致竞态或漏唤醒。
- 多个请求同时命中未缓存 key → 全部进入加载逻辑,而非仅一个执行、其余等待
- 用
select等 channel 时没设超时,阻塞住整个 batcher - batch size 固定但实际请求分布稀疏,导致延迟毛刺(等不满就永远不发)
用 singleflight 库实现最简可靠的合并
golang.org/x/sync/singleflight 就是为这问题写的:对相同 key 的并发调用,只让第一个执行函数,其余等待其返回结果。它不依赖 batch,也不要求 key 有规律,适合「查单个 ID」这类高频重复请求。
示例:用户中心频繁查 GetUserProfile(id)
立即学习“go语言免费学习笔记(深入)”;
var group singleflight.Group
<p>func GetUserProfile(id int) (Profile, error) {
v, err, _ := group.Do(fmt.Sprintf("user:%d", id), func() (interface{}, error) {
return fetchFromDB(id) // 真实 IO 操作
})
if err != nil {
return Profile{}, err
}
return v.(Profile), nil
}-
group.Do第二个参数必须是无参函数,返回(interface{}, error);注意类型断言 - key 字符串要能区分不同请求(别用裸
id,加前缀防冲突) - 默认不带超时,需在
fetchFromDB内部控制,或包装一层带 context 的调用
需要按时间/数量触发合并?自己实现 batcher
当请求 key 不重复(如查不同订单)、但希望聚合发批接口(GetOrders([]int{1,2,3}))时,singleflight 失效,得上定时+计数双触发的 batcher。
核心结构:一个接收请求的 channel,一个后台 goroutine 定期 flush。
type Batcher struct {
reqCh chan batchReq
}
<p>type batchReq struct {
id int
done chan<- result
}</p><p>type result struct {
id int
data Order
err error
}</p><p>func (b *Batcher) GetOrder(id int) (Order, error) {
done := make(chan result, 1)
b.reqCh <- batchReq{id: id, done: done}
r := <-done
return r.data, r.err
}- 后台 goroutine 用
time.AfterFunc或time.Ticker控制 flush 间隔,避免纯time.Sleep占 goroutine - 每次 flush 前检查 pending 列表长度,达到阈值(如 100)立即发,不等时间
- 每个
donechannel 必须带缓冲(make(chan result, 1)),否则可能死锁 - 注意 panic 恢复:batch 请求失败不能让整个 batcher 崩溃
合并模块上线前必须验证的三件事
合并逻辑一旦出错,轻则数据错乱,重则雪崩。不是跑通 demo 就完事。
- 压测时观察
singleflight的group.Do返回的shared值:true 越多,合并越有效;若长期为 false,说明 key 设计不合理或请求太分散 - batcher 的 flush 间隔设为 10ms,但在真实链路中,上游 HTTP handler 可能已自带 50ms 超时,合并反而拖慢响应 —— 必须端到端测 P99 延迟
- 下游批接口若返回部分成功(如 100 个 ID 中 3 个 404),你的合并模块得能拆分错误,原样透传给对应
donechannel,不能全盘失败
合并不是银弹。key 粒度、batch size、flush 时机,全得贴着业务流量分布调。线上先开 5% 流量,看监控里重复请求下降比例和延迟变化,再决定要不要扩。


















