编码母题 / P0 / 异步编程 / 04

并发请求去重与单次执行

先读清题意与边界,独立尝试,再展开思路并对照可运行的参考代码。

重要程度必练难度中等预计完成25–35 分钟

难度按 LeetCode 简单/中等/困难三档估计;工程题并非 LeetCode 官方题目。用时指独立编码并检查主要边界的参考时间,因经验而异。

题目是什么

相同 key 的并发调用共享同一次在途任务,不同 key 相互独立。任务结束后删除在途记录;失败也不能被永久缓存。

输入输出示例

两个同时请求同一配置 key 的调用只执行一次加载,但完成后下一次调用会重新执行。

练习起手式与验证

复制代码后补全空方法,再运行下方三组验证;复杂题所需的辅助类型与输入准备一并放在代码块中。

/*
 * 题目:并发请求去重与单次执行
 * 要求:相同 key 的并发调用共享同一次在途任务,不同 key 相互独立。
 * 任务结束后删除在途记录;失败也不能被永久缓存。
 * 示例:两个同时请求同一配置 key 的调用只执行一次加载,但完成后下一次调用会重新执行。
 */
class Singleflight {
  constructor() {}
  do(key, task) {}
  has(key) {}
}

(async () => {
  const group = new Singleflight();
  let calls = 0;
  let release;
  const gate = new Promise((resolve) => {
    release = resolve;
  });
  const task = async () => {
    calls += 1;
    await gate;
    return 42;
  };
  const a = group.do("x", task);
  const b = group.do("x", task);
  console.log(a); // 预期:b
  console.log(calls); // 预期:0
  release();
  console.log(await Promise.all([a, b])); // 预期:[42, 42]
})().catch(console.error);

解题思路

展开思路、不变量与常见边界

关键切入点:Map 保存 key 到在途 Promise;必须先登记再暴露,并在完成时按身份清理。

核心模型:Map<key, Promise>。必须先把 Promise 放入 Map,再暴露给调用方; 在 finally 中删除,并通过身份比较避免旧请求误删同 key 的新请求。 不变量:同一时刻每个 key 最多有一个在途 Promise;完成后无残留记录。 时间复杂度 O(1),空间复杂度 O(k),k 为同时在途的不同 key 数。 常见错误:只在成功时清理;缓存最终值导致语义变成永久缓存;直接 await 后 才写入 Map,形成竞态;一个调用方取消共享底层请求,误伤其他调用方。 迁移:Token 刷新、配置加载、缓存回源、Agent 同一 Tool 查询合并。

参考代码与验证

“关键机制”注释用于解释实现,下载链接提供未经页面注释修改的原始 JS 文件。

// 关键机制:Map 保存 key 到在途 Promise;必须先登记再暴露,并在完成时按身份清理。
class Singleflight {
  constructor() {
    this.inflight = new Map();
  }

  do(key, task) {
    if (typeof task !== "function")
      throw new TypeError("task must be a function");
    if (this.inflight.has(key)) return this.inflight.get(key);

    const promise = Promise.resolve().then(task);
    this.inflight.set(key, promise);
    promise
      .finally(() => {
        if (this.inflight.get(key) === promise) this.inflight.delete(key);
      })
      .catch(() => {});
    return promise;
  }

  has(key) {
    return this.inflight.has(key);
  }
}

// 三组以上可直接运行的验证;异步题使用独立执行块。
(async () => {
  const group = new Singleflight();
  let calls = 0;
  let release;
  const gate = new Promise((resolve) => {
    release = resolve;
  });
  const task = async () => {
    calls += 1;
    await gate;
    return 42;
  };
  const a = group.do("x", task);
  const b = group.do("x", task);
  console.log(a); // 预期:b
  console.log(calls); // 预期:0
  release();
  console.log(await Promise.all([a, b])); // 预期:[42, 42]
  console.log(calls); // 预期:1
  await Promise.resolve();
  console.log(group.has("x")); // 预期:false

  const error = new Error("boom");
  console.log(await Promise.resolve(group.do("e", () => Promise.reject(error))).then(() => false, () => true)); // 预期:true(拒绝)
  await Promise.resolve();
  console.log(group.has("e")); // 预期:false
  console.log(await group.do("e", () => 7)); // 预期:7


})().catch(console.error);

复杂度分析

时间复杂度:平均 O(1)/次。在途 Promise 按 key 通过 Map 查找、登记和清理。

空间复杂度:O(k)。Map 至多保留 k 个同时在途的不同 key。

下载原始 JS 文件