前端并发控制:100个请求同时发,系统扛不住怎么办

更新日期: 2026-04-22 阅读: 16 标签: 请求

面试的时候经常遇到这道题:100个请求一起打过来,怎么处理?

很多人第一反应是用Promise.all。代码写出来也没错,问题是100个请求会同时发出去。如果请求的是自己的服务,可能把连接池打满。如果调的是第三方接口,对方可能直接返回429限流。浏览器端还有并发连接数限制,Node端还有socket、超时、重试这些问题。最后结果不是慢,就是抖。

这道题真正想问的是:你有没有并发控制的意识。


错误写法长什么样

先看一个典型的错误写法:

const tasks = ids.map(id => () => requestUser(id));

Promise.all(tasks.map(fn => fn()))
  .then(res => console.log(res))
  .catch(err => console.error(err));

代码本身没毛病。但100个请求同时发出去,下游系统大概率扛不住。


正确做法:做一个并发池

真正该做的是:同时只跑5个或10个请求,跑完一个再补下一个。

核心就两步:

  1. 把100个请求先存起来,不要一起执行

  2. 启动固定数量的worker,让它们从任务队列里一个一个取


一个能用的版本

先写一个基础版本,守住三个关键点:

  • 最大并发数可控

  • 所有结果最终能拿到

  • 结果顺序和原始任务顺序一致

async function promisePool(taskFns, limit = 5) {
  const results = new Array(taskFns.length);
  let nextIndex = 0;

  async function worker() {
    while (true) {
      const current = nextIndex++;
      if (current >= taskFns.length) {
        break;
      }

      try {
        results[current] = await taskFns[current]();
      } catch (err) {
        results[current] = {
          error: true,
          message: err.message || 'unknown error'
        };
      }
    }
  }

  const workers = Array.from(
    { length: Math.min(limit, taskFns.length) },
    () => worker()
  );

  await Promise.all(workers);
  return results;
}


怎么用

function mockRequest(id) {
  const delay = Math.floor(Math.random() * 2000);

  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id === 17 || id === 42) {
        reject(new Error(`request failed: ${id}`));
        return;
      }

      resolve({
        id,
        delay,
        data: `user-${id}`
      });
    }, delay);
  });
}

const taskFns = Array.from({ length: 100 }, (_, i) => {
  const id = i + 1;
  return () => mockRequest(id);
});

promisePool(taskFns, 10).then(res => {
  console.log(res);
});


为什么要传函数而不是Promise

有一个细节要主动说出来:为什么taskFns里放的是函数,不是Promise实例?

因为Promise一旦创建,基本就开始执行了。要控制并发,就不能先把100个Promise都new出来,否则控制器还没开始控,请求已经在路上了。

看下面这种写法,看着像控制,实际上已经晚了:

const promises = ids.map(id => requestUser(id)); // 这里请求已经发出去了
await promisePool(promises, 10); // 控制不了

真正该控制的是"启动时机",所以传函数最稳。


带重试的版本

线上场景通常还需要知道:哪几个成功了,哪几个失败了,失败要不要重试。可以稍微包装一下:

async function promisePoolWithRetry(taskFns, limit = 5, retryTimes = 2) {
  const results = new Array(taskFns.length);
  let nextIndex = 0;

  async function runWithRetry(taskFn, taskIndex) {
    let count = 0;

    while (count <= retryTimes) {
      try {
        const data = await taskFn();
        return {
          success: true,
          data,
          taskIndex,
          retry: count
        };
      } catch (err) {
        if (count === retryTimes) {
          return {
            success: false,
            taskIndex,
            retry: count,
            error: err.message || 'unknown error'
          };
        }
        count++;
      }
    }
  }

  async function worker(workerId) {
    while (true) {
      const current = nextIndex++;
      if (current >= taskFns.length) {
        break;
      }

      results[current] = await runWithRetry(taskFns[current], current);
      console.log(`worker-${workerId} finished task-${current}`);
    }
  }

  const workers = Array.from(
    { length: Math.min(limit, taskFns.length) },
    (_, i) => worker(i + 1)
  );

  await Promise.all(workers);
  return results;
}


实际业务场景

比如批量同步用户资料:

async function fetchProfile(userId) {
  const res = await fetch(`https://api.example.com/users/${userId}`);

  if (!res.ok) {
    throw new Error(`http status ${res.status}`);
  }

  return res.json();
}

const userIds = Array.from({ length: 100 }, (_, i) => i + 1);

const taskFns = userIds.map(userId => {
  return () => fetchProfile(userId);
});

const result = await promisePoolWithRetry(taskFns, 8, 1);

const successList = result.filter(item => item.success);
const failList = result.filter(item => !item.success);

console.log('success:', successList.length);
console.log('fail:', failList.length);


并发数设多少合适

并发数不是越大越好。这个值不能随便写50或100,要先看下游是谁。

  • 如果是数据库或者内部服务,要先看连接池、线程池、接口响应时间

  • 如果是第三方HTTP接口,要关注它有没有限流,429之后怎么退避

  • 如果是浏览器端,还要看同域连接数限制

并发控制不是为了优雅,是为了别把系统打毛了。


分批执行的问题

有人会把这题答成"分批执行",写成每10个一组:

async function batchRun(taskFns, batchSize = 10) {
  const results = [];

  for (let i = 0; i < taskFns.length; i += batchSize) {
    const currentBatch = taskFns.slice(i, i + batchSize);
    const batchRes = await Promise.all(currentBatch.map(fn => fn()));
    results.push(...batchRes);
  }

  return results;
}

这也能做,但不够好。因为一批里只要有一个慢请求,后面的任务就全得等。前面9个早跑完了,也不能补位。这个吞吐其实不高,空转时间比较多。

所以用worker池这种写法更好:谁跑完谁领下一个,不会傻等。

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/13669

相关推荐

flutter之网络请求dio封装,拦截器的封装

flutter一直很火的网络请求插件dio,直接上代码,写成一个类,可以直接使用,包含请求的封装,拦截器的封装

ajax异步请求302分析

遇到这样一种情况,打开网页两个窗口a,b(都是已经登录授权的),在a页面中退出登录,然后在b页面执行增删改查,这个时候因为授权原因,b页面后端的请求肯定出现异常(对这个异常的处理,进行内部跳转处理),b页面中的ajax请求的回调中就会出现问题

nginx 301跳转https后post请求失效问题解决

强制把http请求跳转到https,结果发现App有部分的功能不能使用,因为App一共设置了4种请求方式,分别是GET,POST,DELETE和OPTIONS方式,设置301跳转后所有的请求方法都变成了GET方式,导致一些功能无法正常使用.

Js两个异步请求 同步合并数据

业务代码经常会有 两个不一样的请求,拿到数据后合并成新数组的操作。但是在异步请求中我们不知道哪个请求的回调更快返回,从而使代码的合并时间无法确定。这就需要在两个异步请求都完成后再做数据处理。

http请求过程的7个步骤

HTTP通信机制是在一次完整的HTTP通信过程中,Web浏览器与Web服务器之间将完成下列7个步骤:建立TCP连接、Web浏览器向Web服务器发送请求命令、Web浏览器发送请求头信息、 Web服务器应答

http请求的几种类型

http请求中的8种请求方法:opions 返回服务器针对特定资源所支持的HTML请求方法 ,Get 向特定资源发出请求,Post 向指定资源提交数据进行处理请求

node.js含有%百分号时,发送get请求时浏览器地址自动编码的问题

目前浏览器会对地址,进行编码,比如这个文件名:在发到后台时,会自动编码成:不过如果文件名中含有%百分号,编码过程则会出现问题,如

HTTP请求报文和响应报文

GET:请求获取Request—URL所标识的资源,POST:在Request—URL所标识的资源后附加资源,HEAD:请求获取由Request—URL所标识的资源的响应消息报头,PUT:请求服务器存储一个资源,由Request—URL作为其标识

ajax中options请求的理解

这个概念听着有点耳生,嗯是我自己这么说的。我们可以把浏览器自主发起的行为称之为“浏览器级行为”。之所以说options是一种浏览器级行为,是因为在某些情况下,普通的get或者post请求回首先自动发起一次options请求

HTTP请求的11个处理阶段

几乎所以有关Nginx书只要是讲深入点的就会讲到Nginx请求的11个处理阶段,要记住这些真是不易,人脑特别不擅长记住各种东西,只能做些索引罢了,能做到知道这个知识点在哪儿能找到不就行了,可是你去面试还是问这些理论,所以这里汇总下记录如下

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!