1)SingleFlight 解决的到底是什么问题?
很多线上问题本质都是“并发放大”:
- 同一时间多个请求都发现 Token 过期 → 都去刷新 → refresh 被打爆,甚至互相把 token 搞失效
- 热点详情页:同一个
id在列表预取、详情页、评论模块里同时拉 → 同一个接口回源 8 次 - 缓存击穿:TTL 到期的一瞬间,大家同时 miss → 同一个 key 回源 N 次
- 甚至不是网络:多个地方同时读同一条 DB 记录 → IO/锁竞争放大,掉帧
SingleFlight 的思想很简单:同一个 key 在同一时间窗口内,只允许“第一个”去真正回源,其余请求不重复回源,只等待复用结果。
你可以把它理解成“每个 key 一把小闸门”。
2)这个 Demo 怎么演示 SingleFlight 的?
在 ViewController 里有:
- 一个
SingleFlight开关 - 7 个按钮(对应 7 个常见必用场景)
- 每次点击都触发
burst x8(8 个并发请求)
UI 顶部会显示一行指标(示例):
origin:真实回源次数coalesced:被合并的次数(多少请求没有自己回源)stale:用了陈旧缓存返回(SWR)bypass:绕过 singleflight 的次数(比如开关关闭、或不可合并副作用)reject:等待队列太大被拒绝 jointimeout:singleflight 超时兜底次数waitP95 / waitP99:等待时延分位inflightMax:同一时刻有多少个 key 在回源
直观现象:
- singleflight=ON:一次 burst,
origin大约 +1,coalesced大约 +7 - singleflight=OFF:一次 burst,
origin大约 +8,coalesced不怎么增长
3)SingleFlight 核心实现长什么样?
核心在 SingleFlight:
inflight: [String: Entry]Entry里保存:callbacks:所有等待者的回调startedAt:用于统计等待时间timeoutWorkItem:兜底超时,防“回调永不回来”导致 inflight 泄漏start:真实回源闭包(用于失败时“让 joiner 单独重试”)
流程(通俗版):
- 来了一个请求
(key) - 如果
inflight[key]已存在:把自己的 completion 加进队列 → 直接返回(不回源) - 如果不存在:创建 entry,把自己也放进队列 → 执行真实回源
start - 回源结束:
- remove
inflight[key] - 把同一个结果广播给所有等待者
- remove
4)为什么要做“生产边界”?Demo 里补了哪些坑位?
只做“合并”很容易出事故,生产通常要补这些边界。这个 Demo 已经把它们落成了代码策略:
-
不可合并副作用绕过
写接口/扣费/上报等不能随便合并,否则语义会被破坏。Demo 用policy.effect控制是否 bypass。 -
超时兜底防 inflight 泄漏
如果真实回源永远不回调,inflight 会一直卡住,后续请求永远 join。Demo 用timeoutMs+timeoutWorkItem清理并给等待者返回超时。 -
maxWaiters:热点保护
热点 key 可能瞬间上千等待者,回调队列会爆内存/回调风暴。Demo 用maxWaiters限流,多出来的直接 reject。 -
失败传播策略(很关键)
共享失败会“一次失败炸一片”。Demo 支持两种:broadcastSharedFailure:所有等待者共享失败(省 origin)retryJoinersIndividually:首个失败广播给首个请求,其余等待者改为单独重试(避免失败放大,但 origin 会增加)
这些策略在 DemoRepository 里按场景配置。
5)7 个必用场景,在 Demo 里怎么映射?
DemoRepository 提供了 7 个方法(对应 7 个按钮),每个方法都做了两件事:
- 构造 singleflight key(不同维度避免串数据)
- 选择 policy(合并/超时/失败传播/SWR)
举几个例子(你可以直接在文件里搜方法名):
-
Token 刷新:
refreshToken
key 通常要包含userId/authScope/appId/env/tokenFamily,避免跨账号/环境误合并。 -
配置中心:
fetchConfig
常适合stale-while-revalidate:先返回缓存(哪怕 stale),后台 singleflight 刷新。 -
热点详情:
fetchItemDetail
失败可以选择retryJoinersIndividually,降低“偶发失败扩散”。 -
缓存击穿:
getOrLoadCache
miss 时用 singleflight 合并回源,防止同一 cache key 回源 N 次;同时支持 SWR。
6)怎么用这个 Demo 自测理解更深?
建议你按下面顺序点:
- 先开 singleflight,点“热点资源详情(同 id)”几次,看
origin的增长是否接近 1/次 - 关 singleflight,再点同一个按钮,看
origin是否接近 8/次 - 点“缓存失效回源(击穿)”,观察
stale是否增长(如果配置了 SWR 且有旧值) - 多点几次 burst,看
waitP95/waitP99,理解“合并的代价是排队等待”
截图: