前端并发控制:100个请求同时发,系统扛不住怎么办
面试的时候经常遇到这道题: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个请求,跑完一个再补下一个。
核心就两步:
把100个请求先存起来,不要一起执行
启动固定数量的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池这种写法更好:谁跑完谁领下一个,不会傻等。
本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!