惊群问题
高负载下的缓存未命中是经典故障模式:每个请求检查缓存、未命中、调用数据库,数据库在缓存刚失效时承受尖峰。单飞确保只有第一个请求执行查询,其余等待其结果。
type Entry<T> = { promise: Promise<T> };
class SingleFlight {
private inflight = new Map<string, Entry<unknown>>();
async run<T>(key: string, operation: () => Promise<T>): Promise<T> {
const existing = this.inflight.get(key) as Entry<T> | undefined;
if (existing) return existing.promise;
const entry: Entry<T> = { promise: operation() };
this.inflight.set(key, entry as Entry<unknown>);
try {
return await entry.promise;
} finally {
this.inflight.delete(key);
}
}
}
作用域与生命周期
键必须精确标识工作:缓存键、用户 ID 或令牌刷新身份。在 finally 中删除条目,避免失败污染后续调用。如果操作可能超过调用方截止时间,应支持取消,但不要取消其他等待者共享的工作。
何时使用
用于读穿缓存、DNS 或元数据解析、令牌刷新。与缓存相比,单飞减少并发重复工作,但不会让未来请求更快;与锁相比,它让所有调用方获得结果,而不必串行执行重复操作。
陷阱
有副作用的操作不应被去重,除非副作用本身幂等。把同一个可变对象返回给所有调用方会引入共享状态 bug。在多实例部署中,进程内 Map 仍允许每个实例调用一次;当后端无法承受这种扇出时,应结合分布式缓存或锁。