ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

前端面试题:如何控制批量接口请求的最大并发数

前端面试题:如何控制批量接口请求的最大并发数 前端面试题如何控制批量接口请求的最大并发数 题目一次性需要发送 100 条接口请求不能全部并发会压垮后端也不能完全串行太慢如何控制最大并发数请手写一个并发控制器。 核心思路一句话用任务队列 运行计数器 完成即补位的并发池模型将并发数锁定在阈值内兼顾吞吐量与服务端稳定性。 主要矛盾 vs 次要矛盾层级矛盾说明主要矛盾前端吞吐量 vs 后端承载上限并发过高→网关限流/502并发过低→用户等待过长次要矛盾①结果顺序 vs 完成顺序并发执行完成顺序不确定业务可能需要按原始顺序聚合次要矛盾②失败处理 vs 整体流程单条失败不应阻塞其余任务但需可追溯次要矛盾③可中断性 vs 已发出请求用户取消时已 in-flight 的请求无法真正撤回只能丢弃响应 三种方案对比┌─────────────────────────────────────────────────────────────────┐ │ 方案APromise.all 全并发 │ │ 100个请求同时发出 → 瞬间打满网关 → 触发限流/超时 → ❌ 不可用 │ ├─────────────────────────────────────────────────────────────────┤ │ 方案Bfor await 串行 │ │ 一个完成再发下一个 → 100×RTT → 速度极慢 → ❌ 浪费网络空闲 │ ├─────────────────────────────────────────────────────────────────┤ │ 方案C并发控制池限流池 ✅ 最优解 │ │ 固定 N 个并发槽位完成一个补一个 → 效率 稳定性兼得 │ └─────────────────────────────────────────────────────────────────┘维度Promise.all串行 await并发池并发数100不可控1N可配置总耗时≈1×RTT理想≈100×RTT≈⌈100/N⌉×RTT服务端压力极高易崩极低可控适用场景≤5个无依赖请求强顺序依赖批量操作推荐️ 并发池架构图文本版┌──────────────────────────────┐ │ 任务队列 (Queue) │ │ [task5, task6, ... task100] │ └──────────┬───────────────────┘ │ shift() 取队首 ▼ ┌─────────────────────────────────────────────────────┐ │ 并发调度器 (Scheduler) │ │ │ │ runningCount: 当前运行数 maxConcurrency: N │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ Slot 1 │ │ Slot 2 │ │ Slot N │ ← N个槽位 │ │ │ task1 ▶ │ │ task2 ▶ │ │ task3 ▶ │ │ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │done │done │done │ │ ▼ ▼ ▼ │ │ runningCount-- → 从队列取新任务 → runningCount │ └─────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────┐ │ 结果收集 (按原始index存放) │ │ results[0..99] │ └──────────────────────────────┘ 终止条件queue.length 0 runningCount 0 核心流程文本流程图开始 │ ├─ 1. 将 100 个请求封装为 () Promise 函数推入 taskQueue │ ├─ 2. 初始化循环 min(maxConcurrency, queue.length) 次 │ 调用 runNext() 启动首批任务 │ ├─ 3. runNext() 内部 │ ├─ runningCount │ ├─ 执行 task() │ ├─ .then → 收集结果到 results[index] │ ├─ .catch → 记录错误 / 触发重试 │ └─ .finally → │ ├─ runningCount-- │ ├─ 队列非空→ runNext()补位 │ └─ 队列空 runningCount0→ resolve 整体 Promise │ └─ 4. 返回 Promiseresults[]调用方 await 获取全部结果 完整示例代码生产级classConcurrencyScheduler{constructor({maxConcurrency5,retries0,retryDelay1000}{}){this.maxmaxConcurrency;this.retriesretries;this.retryDelayretryDelay;this.queue[];// 等待队列this.running0;// 当前运行数this.results[];// 按原始顺序收集结果this.errors[];// 错误收集this.isPausedfalse;// 中断标志this._resolvenull;}/** 添加任务task 是返回 Promise 的函数index 用于结果排序 */addTask(task,index){this.queue.push({task,index});}/** 启动调度 */start(){returnnewPromise((resolve){this._resolveresolve;constinitialCountMath.min(this.max,this.queue.length);for(leti0;iinitialCount;i){this._runNext();}// 边界队列为空0个任务if(this.queue.length0this.running0){resolve(this.results);}});}/** 中断清空队列不再补位已发出的请求无法撤回但结果会被丢弃 */abort(){this.isPausedtrue;this.queue[];}async_runNext(){if(this.isPaused||this.queue.length0){this._checkDone();return;}const{task,index}this.queue.shift();this.running;try{constresultawaitthis._execWithRetry(task);this.results[index]{status:fulfilled,value:result};}catch(err){this.results[index]{status:rejected,reason:err};this.errors.push({index,error:err});}finally{this.running--;this._runNext();// 补位}}/** 带重试的执行 */async_execWithRetry(task,attempt0){try{returnawaittask();}catch(err){if(attemptthis.retries){awaitthis._sleep(this.retryDelay*(attempt1));// 递增延迟returnthis._execWithRetry(task,attempt1);}throwerr;}}_checkDone(){if(this.queue.length0this.running0this._resolve){this._resolve(this.results);}}_sleep(ms){returnnewPromise((r)setTimeout(r,ms));}}使用示例// 模拟 100 个接口请求consturlsArray.from({length:100},(_,i)/api/data/${i});constschedulernewConcurrencyScheduler({maxConcurrency:6,// 最大 6 个并发retries:2,// 失败最多重试 2 次retryDelay:1000,});urls.forEach((url,index){scheduler.addTask(()fetch(url).then(resres.json()),index// 保证结果按原始顺序);});constresultsawaitscheduler.start();console.log(results);// [{status:fulfilled, value:...}, ...] 长度100顺序一致中断示例// 用户点击取消按钮cancelBtn.onclick()scheduler.abort(); 使用场景场景说明推荐并发数批量导入Excel→逐条调接口几千条数据逐条提交5~10批量查询详情列表页补充信息100个ID查详情6~8批量文件上传分片大文件切片并发上传3~6受带宽限制图片批量压缩/预览前端本地处理 上传4~6爬虫/数据采集合规前提下控制对目标站点压力2~4⚠️ 边界场景 注意事项边界处理方式任务数为 0start() 直接 resolve([])任务数 maxConcurrency全部立即执行无需排队单个任务永远 pending加超时包装Promise.race([task(), timeout(ms)])全部失败收集 errors不阻塞整体流程最终统一上报内存溢出10万任务分批 push 入队 / 配合 Web Worker / 后端分片接口浏览器同域并发限制HTTP/1.1 同域最多 6 连接HTTP/2 多路复用无此限制AbortController 真正取消配合 fetch 的 signal 参数abort 时真正断开连接超时包装补充functionwithTimeout(promiseFn,ms10000){return()Promise.race([promiseFn(),newPromise((_,reject)setTimeout(()reject(newError(TIMEOUT)),ms)),]);}scheduler.addTask(withTimeout(()fetch(url),8000),index); 与相关 API 的关系补充知识点API作用局限Promise.all全并发全部成功才 resolve无法限流一个失败全挂Promise.allSettled全并发收集所有结果同样无法限流Promise.any全并发一个成功即 resolve场景不同p-limitnpm现成并发控制库原理同本文生产可直接用p-queuenpm支持优先级、暂停、事件更重适合复杂调度面试手写考察的是原理理解生产中可用p-limit一行搞定importpLimitfromp-limit;constlimitpLimit(6);constresultsawaitPromise.all(urls.map((url,i)limit(()fetch(url)))); 面试满分答案话术版面对大批量接口请求我采用并发池限流方案设定最大并发阈值如 6将所有请求封装为返回 Promise 的函数存入任务队列初始启动 N 个任务占满并发槽位任一任务完成后计数器减一立即从队列头部取出新任务补位计数器加一队列为空且运行数为零时整体完成按原始顺序返回结果数组。这样做既避免瞬时并发打崩网关又比串行高效数倍。在此基础上我还做了三层扩展失败重试单任务失败自动重试 N 次递增延迟重试耗尽才标记失败中断取消暴露 abort 方法清空队列、停止补位配合 AbortController 真正取消 in-flight 请求超时兜底用 Promise.race 包装防止单任务 hang 住占满槽位。如果数据量特别大上万条还会结合后端分页/分片接口从源头减少请求数前端再配合并发池双管齐下。以上即本题完整核心干货掌握队列 计数器 补位三要素即可应对所有变体。
返回列表