← 中文资源

简体中文版本

用 Singleflight 合并重复加载

并发调用同一 key 时只让一个调用者真正执行加载,其余调用者等待并共享结果,同时返回是否由自己发起加载。

阅读英文原文

功能说明

Group.Do 保证同一个 key 同时只有一个加载在执行。第一个调用者创建 call 并真正执行 load,其余调用者在锁外等待同一个 WaitGroup,随后共享结果与错误。返回值中的布尔量表示当前调用者是否是发起加载的 leader;loads 计数器用于验证实际只加载了一次。

关键代码

g.mu.Lock()
if existing, ok := g.calls[key]; ok {
	g.mu.Unlock()
	existing.wg.Wait()
	return existing.val, false, existing.err
}
current := &call[T]{}
current.wg.Add(1)
g.calls[key] = current
g.loads.Add(1)
g.mu.Unlock()

等待必须在释放锁之后进行,否则 leader 完成时无法重新加锁删除条目,造成死锁。

使用注意

  • 所有等待者共享同一个结果和错误;如果加载失败,调用方需要自行决定是否重试。
  • key 的粒度决定合并范围,粒度过粗会把不同请求错误地合并。
  • leader 完成到删除 map 条目之间存在很短窗口,新来的调用可能看到已完成的条目并立即返回,这是可接受的。
  • 如果 load 发生 panic,wg.Done 不会执行,等待者会永久阻塞;生产代码应使用 defer。
  • 该实现不缓存结果,只合并进行中的请求;常与带 TTL 的缓存配合使用。

验证方式

示例启动 8 个调用者同时请求 user:42,加载函数睡眠 30 毫秒。输出中应只有 1 行 leader=true,其余为 leader=false,最后打印 loads: 1。把 key 改成每个调用者不同,可验证不再合并。