人人都会AI编程

12.4 异步并发控制:限流、重试、超时、竞态处理

更新时间:2026-07-11

在真实的业务场景中,异步操作远不止“发一个请求,等结果”这么简单。很多时候我们需要同时发起多个请求以提高效率,但又不能无限制地并发;网络可能抖动,请求可能失败,需要安全的自动重试;某些操作拖得太久,必须设置超时结束;还有多个异步操作的结果可能交错到达,导致界面显示与用户操作不一致。这一节,我们就来解决这些异步编程中的“进阶控制”问题。

12.4.1 并发限流

假设需要调用一个分页接口抓取 100 页数据,如果同时发出 100 个请求,不仅会对服务端造成冲击,浏览器也可能因为连接数限制导致部分请求阻塞,整体效率并不一定高。更好的做法是限制同时请求的数量,例如同时只允许 3 个请求在执行,一个完成后再启动下一个。

通用限流函数实现

async function asyncPool(limit, tasks) {
  const results = [];
  const executing = new Set();

  for (const task of tasks) {
    const promise = Promise.resolve().then(() => task());
    results.push(promise);
    executing.add(promise);

    const clean = () => executing.delete(promise);
    promise.then(clean, clean);

    if (executing.size >= limit) {
      await Promise.race(executing);
    }
  }

  return Promise.all(results);
}

用法示例:同时最多 3 个请求

const urls = ['/api/data?page=1', '/api/data?page=2', /* ... 100个 */];
const tasks = urls.map(url => () => fetch(url).then(res => res.json()));

asyncPool(3, tasks).then(allResults => {
  console.log('所有请求完成', allResults);
});

核心思路:维护一个执行中的 Promise 集合,当集合大小达到上限时,用 Promise.race 等待任一完成,腾出空位后再继续添加新任务。实际开发中也可以直接使用成熟的工具库,如 p-limit

12.4.2 异步重试

网络请求可能因为临时故障而失败,直接在代码里 try/catch 然后放弃未免太脆弱。一个健壮的系统应当具备自动重试机制,并配合适当的延迟(避免“惊群效应”)。

带重试的 fetch 封装

async function fetchWithRetry(url, options = {}, retries = 3, backoff = 300) {
  for (let i = 0; i < retries; i++) {
    try {
      const response = await fetch(url, options);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return response;
    } catch (err) {
      if (i === retries - 1) throw err; // 最后一次仍失败,抛出错误
      // 指数退避延迟:第1次等300ms,第2次等600ms...
      await new Promise(resolve => setTimeout(resolve, backoff * (i + 1)));
    }
  }
}

用法

const data = await fetchWithRetry('/api/unreliable-endpoint')
  .then(res => res.json());

重试策略可以根据业务需求调整:固定延迟、指数退避、加上随机抖动(防止大量客户端同时重试造成服务端压力)。

12.4.3 超时处理

有些异步操作没有内置的超时机制(例如 fetch 自身支持 AbortController 但需要手动封装),或者你想要强制一个 Promise 必须在规定时间内完成,否则按失败处理。

为任何 Promise 添加超时

function withTimeout(promise, ms) {
  return Promise.race([
    promise,
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error('操作超时')), ms)
    )
  ]);
}

使用 AbortController 取消底层 fetch(推荐)

async function fetchWithTimeout(url, options = {}, timeout = 5000) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeout);

  try {
    const response = await fetch(url, { ...options, signal: controller.signal });
    return response;
  } finally {
    clearTimeout(timer);
  }
}

这种方式不仅在外层抛出了超时错误,还真正中止了网络请求,释放了连接资源。

12.4.4 竞态处理

在用户快速切换页面、连续点击按钮或输入搜索关键词时,多个异步操作可能“后发先至”,导致最后渲染的结果与最后一次操作意图不匹配。例如:用户快速输入“abc”,依次发出 3 个搜索请求,但第二个请求(搜索“ab”)的网络返回较慢,结果在第三个请求(“abc”)之后到达,搜索结果就会被错误地显示为“ab”的结果。这就是典型的异步竞态问题。

解决思路:丢弃“过时”的响应

方法一:使用标志位

let requestId = 0;

async function search(keyword) {
  const currentId = ++requestId;
  const results = await fetch(`/api/search?q=${keyword}`).then(res => res.json());
  if (currentId === requestId) {
    // 只有最新一次的请求结果才会更新 UI
    updateUI(results);
  }
}

方法二:利用 AbortController 取消前一个请求

let lastController = null;

async function search(keyword) {
  if (lastController) {
    lastController.abort(); // 取消上一个请求
  }
  const controller = new AbortController();
  lastController = controller;
  try {
    const results = await fetch(`/api/search?q=${keyword}`, {
      signal: controller.signal
    }).then(res => res.json());
    updateUI(results);
  } catch (err) {
    if (err.name === 'AbortError') {
      // 请求被取消,这是预期行为,无需处理
      console.log('请求被取消');
    } else {
      console.error(err);
    }
  }
}

现代前端框架通常有更上层的方法来处理竞态。例如 React 的 useEffect 中清理副作用,或用 useRef 存储最新请求标识;Vue 的 watchEffectonCleanup 也能配合取消。但原理都离不开以上两种模式。

12.4.5 综合案例:分页数据全量获取

下面是一个结合限流、重试、超时的实用场景:将一个 100 页的数据全部下载下来,每页最多并发 5 个请求,每个请求超时 10 秒,失败自动重试 2 次。

async function fetchAllPages(baseUrl, totalPages) {
  const tasks = Array.from({ length: totalPages }, (_, i) => async () => {
    const url = `${baseUrl}?page=${i + 1}`;
    return fetchWithTimeoutAndRetry(url);
  });
  return asyncPool(5, tasks);
}

async function fetchWithTimeoutAndRetry(url, retries = 2, timeout = 10000) {
  for (let i = 0; i <= retries; i++) {
    try {
      const controller = new AbortController();
      const timer = setTimeout(() => controller.abort(), timeout);
      const response = await fetch(url, { signal: controller.signal });
      clearTimeout(timer);
      if (!response.ok) throw new Error('Status not OK');
      return response.json();
    } catch (err) {
      if (i === retries) throw err;
      await new Promise(r => setTimeout(r, 500 * (i + 1)));
    }
  }
}

这样的组合策略能显著提升大批量异步任务处理的鲁棒性与效率,是真实工程中不可或缺的能力。

掌握了异步并发控制,再结合下一节的错误捕获与全局监控,你就有了一套完整的异步编程“防崩”体系。