新闻中心

优化大量网络请求:分批处理、并发控制与超时策略

2025-10-31
浏览次数:
返回列表

优化大量网络请求:分批处理、并发控制与超时策略

本文旨在解决前端应用中处理大量网络请求时遇到的api超时、403错误等问题。通过分析常见的错误尝试,文章重点介绍了如何利用bluebird.map进行并发控制,以及如何手动实现分批处理和延迟执行请求,从而有效管理请求负载,避免api限流,确保应用稳定性和用户体验。

引言:处理大量网络请求的挑战

在现代Web应用开发中,我们经常需要向后端API发送大量网络请求,例如批量数据更新、文件上传等场景。然而,当请求数量达到一定规模(如数百甚至上千个)时,直接使用Promise.all等方式并行发送所有请求,往往会导致以下问题:

  1. API 限流或超时:后端服务器可能无法承受瞬间涌入的大量请求,从而触发限流机制,返回429(Too Many Requests)或403(Forbidden)错误,甚至导致请求超时。
  2. 浏览器资源耗尽:在客户端,同时维护大量待处理的网络连接会消耗大量内存和CPU资源,可能导致浏览器卡顿或崩溃。
  3. 用户体验下降:请求失败或长时间无响应都会严重影响用户体验。

以下是一个典型的、可能导致问题的Promise.all实现示例:

const retry = async (requests: API.CarFailedRequest[]) => {
  setIsLoading(true);
  const res = await Promise.all(
    requests.map(async request => {
      try {
        await service.retryFailedRequest(request);
        return { status: true, request };
      } catch (e) {
        return { status: false, request };
      }
    })
  );
  setIsLoading(false);
  return res;
};

这段代码的问题在于,requests.map内部的async request => {...}函数会立即执行,从而创建并启动所有的Promise。Promise.all随后等待所有这些已启动的Promise完成,但此时所有的网络请求已经同时发出,导致上述问题。

错误尝试分析

在尝试解决上述问题时,开发者可能会采取一些看似合理但实际上未能达到预期效果的策略。理解这些尝试为何失败,对于构建正确的解决方案至关重要。

尝试一:Bluebird.map 的误用

一种常见的尝试是引入像 Bluebird 这样的第三方库,它提供了更强大的并发控制能力。然而,如果使用不当,效果可能不佳。

// 错误的 Bluebird.map 使用方式
const retry = async (requests: API.CarFailedRequest[]) => {
  setIsLoading(true);
  const promises = requests.map(async request => {
    try {
      await service.retryFailedRequest(request);
      return true;
    } catch (e) {
      return false;
    }
  });

  await BlueBirdPromise.map(
    promises, // 注意:这里传入的是一个已启动的Promise数组
    async promise => {
      try {
        await promise;
      } catch (err) {
        console.log(err);
      }
    },
    { concurrency: 10 }
  );
  setIsLoading(false);
};

这段代码的问题在于,requests.map 仍然在 BlueBirdPromise.map 调用之前就创建并启动了所有的 Promise。promises 数组中存储的是已经开始执行的网络请求。BlueBirdPromise.map 的 concurrency 选项虽然限制了同时处理 Promise 结果的数量,但它无法阻止这些 Promise 在被传入 map 之前就全部启动。因此,网络请求仍然是瞬间全部发出的。

尝试二:手动分块但未延迟请求启动

另一种思路是手动将请求分成小块(chunks),并尝试在每个块之间添加延迟。

// 错误的 manual chunking 方式
const processPromisesWithDelay = async (promises: any[], delay: number, split: number) => {
  const chunks = [];
  for (let i = 0; i < promises.length; i += split) {
    chunks.push(promises.slice(i, i + split));
  }

  for (const chunk of chunks) {
    await Promise.all(chunk.map((promise: () => any) => promise())); // 问题在这里:promise() 意味着立即执行
    await new Promise((resolve) => setTimeout(resolve, delay * 1000));
  }
};

const retry = async (requests: API.CarFailedRequest[]) => {
  setIsLoading(true);
  const promises = requests.map(async request => {
    await service.retryFailedRequest(request); // 再次:所有请求在这里就启动了
  });
  await processPromisesWithDelay(promises, 5, 5); // 传入的是已启动的Promise数组
  setIsLoading(false);
};

与 Bluebird.map 的误用类似,requests.map 在 processPromisesWithDelay 调用之前就启动了所有请求。即使 processPromisesWithDelay 试图分批处理并添加延迟,它操作的仍然是已经发出的网络请求。在浏览器网络面板中,你会看到所有请求几乎同时处于“pending”状态,只是它们的完成时间被分批等待了,而不是请求的启动时间被分批了。

解决方案一:使用 Bluebird.map 进行并发控制

解决上述问题的关键在于,并发控制应该作用于请求的启动时机,而不是 Promise 的解决时机。Bluebird.map 正是为此设计的,但需要正确使用它。

Mureka Mureka

Mureka是昆仑万维最新推出的一款AI音乐创作工具,输入歌词即可生成完整专属歌曲。

Mureka 1091 查看详情 Mureka

核心思想是:将原始数据(而不是已启动的 Promise)传递给 Bluebird.map,并在 map 的迭代器函数中按需启动每个网络请求。这样,concurrency 选项才能真正限制同时进行的请求数量。

import BlueBirdPromise from 'bluebird'; // 确保已安装 bluebird

const retry = async (requests: API.CarFailedRequest[]) => {
  setIsLoading(true);

  await BlueBirdPromise.map(
    requests, // 直接传入原始的请求数据数组
    async request => { // 在这里,当 Bluebird.map 允许时,才启动请求
      try {
        await service.retryFailedRequest(request);
        // 可以根据需要返回状态或数据
        return { status: true, request };
      } catch (err) {
        console.error("请求失败:", request, err);
        // 返回失败状态,或者根据错误类型进行重试
        return { status: false, request, error: err };
      }
    },
    { concurrency: 10 } // 同时只允许 10 个请求处于活跃状态
  );

  setIsLoading(false);
  // 返回处理结果,Bluebird.map 默认会返回一个包含所有迭代器返回值的数组
  // 例如:const results = await BlueBirdPromise.map(...)
  // return results;
};

代码解析:

  • BlueBirdPromise.map(requests, async request => {...}, { concurrency: 10 }):
    • 第一个参数 requests 是原始的数据数组,每个元素代表一个待处理的请求。
    • 第二个参数是一个 async 函数,它会在 Bluebird.map 内部按并发限制逐个调用。在这个函数内部,await service.retryFailedRequest(request) 才会真正发起网络请求。
    • { concurrency: 10 } 是关键。它告诉 Bluebird 在任何给定时间,最多只能有 10 个 async request => {...} 函数在执行(即最多 10 个网络请求同时进行)。当一个请求完成时,Bluebird 会从队列中取出下一个请求并启动它,直到所有请求都被处理完毕。

这种方法确保了网络请求是分批、有控制地发出的,从而有效避免了API限流和超时问题。

解决方案二:手动实现分批与延迟(更精细控制)

如果不想引入 Bluebird 库,或者需要对分批和延迟有更精细的控制,可以手动实现一个分批处理函数。关键在于,我们需要传递的是返回 Promise 的函数,而不是已经启动的 Promise。

/**
 * 按批次处理异步任务,并在批次之间添加延迟。
 * @param taskFns 数组,每个元素是一个返回 Promise 的函数。
 * @param chunkSize 每批处理的任务数量。
 * @param delayMs 每批次之间的延迟时间(毫秒)。
 * @returns 所有任务完成后的结果数组。
 */
const processTasksInChunksWithDelay = async <T>(
  taskFns: (() => Promise<T>)[],
  chunkSize: number,
  delayMs: number
): Promise<T[]> => {
  const results: T[] = [];
  for (let i = 0; i < taskFns.length; i += chunkSize) {
    const chunkFns = taskFns.slice(i, i + chunkSize);
    // 在这里才调用函数,启动 Promise
    const chunkPromises = chunkFns.map(fn => fn());
    const chunkResults = await Promise.all(chunkPromises);
    results.push(...chunkResults);

    // 如果还有后续批次,则进行延迟
    if (i + chunkSize < taskFns.length) {
      await new Promise(resolve => setTimeout(resolve, delayMs));
    }
  }
  return results;
};

const retryWithManualChunks = async (requests: API.CarFailedRequest[]) => {
  setIsLoading(true);

  // 将每个请求封装成一个返回 Promise 的函数
  const requestTaskFns = requests.map(request => async () => {
    try {
      await service.retryFailedRequest(request);
      return { status: true, request };
    } catch (e) {
      console.error("请求失败:", request, e);
      return { status: false, request, error: e };
    }
  });

  // 调用分批处理函数,每5个请求一批,每批之间延迟5秒
  const allResults = await processTasksInChunksWithDelay(requestTaskFns, 5, 5000);

  setIsLoading(false);
  return allResults;
};

代码解析:

  • requestTaskFns 数组中存储的不再是已启动的 Promise,而是函数。每个函数在被调用时才会返回一个 Promise(即发起网络请求)。
  • processTasksInChunksWithDelay 函数在循环中,每次只取出 chunkSize 个函数。
  • chunkFns.map(fn => fn()) 在这里才真正调用这些函数,启动对应批次的网络请求。
  • await Promise.all(chunkPromises) 等待当前批次的所有请求完成。
  • await new Promise(resolve => setTimeout(resolve, delayMs)) 在批次之间添加了明确的延迟。

这种手动实现方式提供了极大的灵活性,可以精确控制每批次的请求数量和批次间的延迟时间,适用于需要严格遵守API速率限制的场景。

最佳实践与注意事项

  1. 选择合适的并发数/批次大小和延迟:没有一劳永逸的完美值。这取决于你的后端API的承载能力、速率限制以及用户对响应速度的期望。通常需要通过实验和监控来确定最佳参数。
  2. 完善的错误处理与重试机制
    • 在每个请求的 try...catch 块中捕获错误。
    • 对于可重试的错误(如 429, 5xx),可以实现指数退避(exponential backoff)的重试逻辑。
    • 记录失败的请求,以便后续分析或手动处理。
  3. 用户体验考虑
    • 在大量请求处理期间,提供加载指示器 (setIsLoading(true)),避免用户误以为应用无响应。
    • 对于长时间运行的任务,考虑使用Web Workers将网络请求逻辑放到后台线程,避免阻塞主线程。
  4. 监控与日志:在开发和生产环境中,密切监控网络请求的数量、成功率和响应时间。通过日志记录请求失败的详细信息,有助于快速定位问题。
  5. API 设计优化:如果可能,与后端团队协作,探讨是否可以提供批量处理API,从而将多个小请求合并为一个大请求,从根本上减少网络往返次数。

总结

处理大量网络请求是前端开发中的常见挑战。通过本文的探讨,我们了解到直接使用 Promise.all 可能会导致API超时和资源耗尽。关键在于控制请求的启动时机。无论是利用 Bluebird.map 的 concurrency 选项,还是手动实现分批处理和延迟,核心原则都是将大量请求分解为可管理的批次,并控制每个批次内的并发数以及批次间的间隔。通过采纳这些策略并结合最佳实践,开发者可以构建出更健壮、更高效、用户体验更好的应用。

以上就是优化大量网络请求:分批处理、并发控制与超时策略的详细内容,更多请关注其它相关文章!


# 关键在于  # 专注关键词排名资讯  # seo 怎么做外链  # 上海迪士尼推广营销策划  # seo术语基础  # 承德网站推广哪里有卖的  # 网站关键词优化搜索排名  # 藁城营销推广哪家好  # seo流量特点不包括  # 鞍山网站怎么推广  # 无为网站推广公司哪家好  # 这段  # 并在  # 长时间  # 前端  # 而不是  # 重试  # 是一个  # 的是  # 在这里  # 前端应用  # 异步任务  # 应用开发  # ai  # 前端开发  # 后端  # 浏览器 


相关栏目: 【 科技资讯46185 】 【 网络学院92790


相关推荐: 谷歌浏览器如何快速清除某个网站的数据_Chrome网站缓存清理方法  照顾宝贝2小游戏点击立即在线玩  c++如何使用折叠表达式(Fold Expressions)_c++17可变参数模板新技巧  b站赚钱渠道_b站收益来源  Win10快速启动功能利弊分析 Win10开启或关闭快速启动教程【技巧】  品牌机怎么重装系统 联想/戴尔/惠普笔记本恢复出厂系统教程  J*a TimerTask文件监控:HashMap状态管理与常见陷阱规避指南  漫蛙漫画网页端入口 漫蛙2官方正版漫画站点  Python多版本共存与虚拟环境管理深度指南  FullCalendar 自定义按钮样式定制指南  b站怎么看视频的弹幕数量_b站弹幕数量查看方法  C#使用XPath查询节点时出错? 常见语法错误与调试技巧  荒野行动PC版怎么注册_荒野行动PC版账号注册详细流程图文教程  Lar*el 递归关系中排除指定分支的教程  如何提高微信支付的安全性_微信支付安全防护与设置建议  Golang并发任务中错误如何聚合_Golang goroutine error收集方式  J*a里如何实现订单支付与库存同步功能_支付库存同步项目开发方法说明  解决深度学习模型训练初期异常高损失与完美验证准确率问题  Node.js中HTML按钮与J*aScript函数交互的正确姿势  在Go Martini框架中高效服务动态生成图像的实践指南  Pygame教程:解决用户输入与游戏状态更新不同步问题  sublime如何处理大型CSV文件的列对齐_sublime高级表格编辑插件指南  sublime怎么预览Markdown渲染效果_Markdown Preview插件 for sublime教程  c++如何实现一个简单的ECS框架_c++数据驱动设计与游戏开发  Win10系统服务哪些可以禁用 Win10安全优化服务列表【干货】  React/Next.js中实现列表项的动态选择与移动  Python实时数据流中的动态最值查找策略  荣耀Play7T运行卡顿解决_荣耀Play7T性能优化  高德地图家和公司地址在哪设置 高德地图通勤路线设置方法【超详细】  J*aScript对象创建方式_J*aScript设计模式应用  Composer如何在生产环境安全地执行composer update  Windows10怎么开启存储感知 Windows10系统设置自动清理临时文件释放C盘空间【教程】  漫蛙网页登录入口 漫蛙漫画官方授权网址  Win11怎么查看显卡显存 Win11显示适配器属性及专用视频内存查询  妖精漫画网页版登录入口免费_妖精漫画官网主页直接阅读漫画  必由学官网首页入口 必由学教师网页版登录指南  汽水音乐车机版横屏版7.1 汽水音乐车机版横屏版下载入口  向日葵客户端怎么进行远程CentOS控制_向日葵客户端远程CentOS控制操作教程  谷歌浏览器浏览体验优化_谷歌浏览器新版直连永久可用提示  QQ邮箱官方网页版登录 QQ邮箱个人邮箱快速访问  解决Flask中Quill编辑器内容提交失败及TypeError的指南  在J*a中如何开发简易电子商务商品管理系统_商品管理系统项目实战解析  TikTok搜索结果不显示如何解决 TikTok搜索刷新优化方法  win11 arm版怎么安装 M1/M2 Mac虚拟机安装ARM win11的方法  汽水音乐车机版8.9下载 汽水音乐车机版8.9版本安装入口  快速CSGO开箱网站指南 CSGO开箱平台推荐  微信商城在哪里打开【步骤】  漫画星球免费下拉式入口 漫画星球免费漫画在线阅读网站  微信网页版扫码登录入口 微信网页版二维码登录入口  Lar*el表单中优雅地处理“返回”按钮以规避验证:最佳实践指南 

搜索