Skip to content

关于并发请求

2024-07-01代码笔记

日常开发中的并发请求,一般不会去特地控制,直接依次请求就好,或者使用 Promise.all() 包装一层,等待所有成功的结果。但是当并发请求的数量大起来后,如果不处理并发量,可能会造成较严重的后果。

比如大文件上传的场景,一般大文件上传需要切片分传,如果文件较大,可能会被分成 100 个小块分别请求,那这种同时 100 条的并发请求,大概率会有以下几个问题:

  1. 浏览器会限制,首先浏览器会限制同一域名下的并发请求数量,100 个请求是不会立刻并发执行的,会排队处理,可能导致部分请求等待较长时间才开始;
  2. 服务器压力,即使绕过了浏览器的并发请求数量,短时间内大量的请求也会给服务器带来较大压力;
  3. 网络拥堵,大量并发请求会占用较多的网络带宽,可能导致其他网络活动受到影响,影响用户体验。

所以我们需要对大并发请求做个控制,即限制每次并发的请求量,比如每次只请求 5 条,100 条请求分 20 次完成。那么怎么实现这种效果呢?

我看好多人会用 Promise.all() 来处理,比如将 100 条异步请求分成 20 个数组,每个数组有 5 个请求,每次将一个请求数组传入 Promise.all()中,当这组数据请求完,再传入下一个数组,以此类推直至请求结束。这种做法在理论上确实可以解决并发问题,之所以是理论上,主要有两个问题:

  1. Promise.all() 不能保证所有的请求结束后再给出结束状态,一旦有一个请求错误,会直接返回错误信息,不管后面的成员还在不在请求中。这样本质上还是无法准确控制同时并发量;
  2. 即使每次传入的一组请求,都是成功状态返回,或者使用Promise.allSettled,保证所有的请求结束后才返回状态。那么还是有效率问题,因为它需要等待这组数据全部请求完才能开启下一轮,如果这一组数据有一个接口需要 10 秒,那么这组数据请求就需要多等 10 秒,显然这问题是很大的。

所以最终还是需要我们自己手动来封装一个并发请求控制,主要实现两个功能:

  1. 可以灵活控制并发数量;
  2. 请求应该是队列形式,当一组请求中的某一条请求结束了,应自动加入下一条请求,以保证最大的效率。

最后,根据需求,参考网上资料,我简单封装了一个并发请求方法,代码如下:

js
function concurRequest(urls, maxNum) {
  return new Promise((resolve) => {
    if (urls.length === 0) {
      resolve([]);
    }
    // 请求的 url 索引
    let index = 0;
    // 已完成请求的数量
    let count = 0;
    // 请求到的结果,每项都是 promise
    const result = [];
    // 请求函数
    async function request() {
      // 缓存原始的下标
      const i = index;
      // 当前要请求的 url
      const url = urls[index];
      // 获取完当前的请求地址后,递增下标,为下一次请求准备
      index++;
      try {
        const res = await fetch(url);
        result[i] = res;
      } catch (err) {
        result[i] = err;
      } finally {
        count++;
        // 如果已请求的数量等于总请求数量,那么就返回结果
        if (count === urls.length) {
          resolve(result);
        }
        if (index < urls.length) {
          request();
        }
      }
    }

    // 第一次触发请求
    for (let i = 0; i < Math.min(maxNum, urls.length); i++) {
      request();
    }
  });
}

concurRequest 方法可以传入两个参数,接口请求路径数组urls和并发数量maxNum,方法会按照传入的最大请求数来控制同时并发量,当某一个请求结束后,会按照接口数组中的顺序自动请求下一条,直至结束。当然这只是一个范例,我们可以根据实际开发需求改动,比如可以传入一组定义好的请求方法,这样每个请求方法的内部都可以灵活控制。