0

0

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

DDD

DDD

发布时间:2025-10-31 21:31:14

|

767人浏览过

|

来源于php中文网

原创

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

本文旨在解决前端应用中处理大量网络请求时遇到的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 正是为此设计的,但需要正确使用它。

Endel.io
Endel.io

Endel是一款可以创造个性化舒缓声音的应用程序,可帮助您集中注意力、放松身心和入睡。

下载

核心思想是:将原始数据(而不是已启动的 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 (
  taskFns: (() => Promise)[],
  chunkSize: number,
  delayMs: number
): Promise => {
  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 选项,还是手动实现分批处理和延迟,核心原则都是将大量请求分解为可管理的批次,并控制每个批次内的并发数以及批次间的间隔。通过采纳这些策略并结合最佳实践,开发者可以构建出更健壮、更高效、用户体验更好的应用。

相关专题

更多
线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

469

2023.08.10

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

469

2023.08.10

golang map内存释放
golang map内存释放

本专题整合了golang map内存相关教程,阅读专题下面的文章了解更多相关内容。

73

2025.09.05

golang map相关教程
golang map相关教程

本专题整合了golang map相关教程,阅读专题下面的文章了解更多详细内容。

25

2025.11.16

golang map原理
golang map原理

本专题整合了golang map相关内容,阅读专题下面的文章了解更多详细内容。

36

2025.11.17

java判断map相关教程
java判断map相关教程

本专题整合了java判断map相关教程,阅读专题下面的文章了解更多详细内容。

31

2025.11.27

promise的用法
promise的用法

“promise” 是一种用于处理异步操作的编程概念,它可以用来表示一个异步操作的最终结果。Promise 对象有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。Promise的用法主要包括构造函数、实例方法(then、catch、finally)和状态转换。

296

2023.10.12

html文本框类型介绍
html文本框类型介绍

html文本框类型有单行文本框、密码文本框、数字文本框、日期文本框、时间文本框、文件上传文本框、多行文本框等等。详细介绍:1、单行文本框是最常见的文本框类型,用于接受单行文本输入,用户可以在文本框中输入任意文本,例如用户名、密码、电子邮件地址等;2、密码文本框用于接受密码输入,用户在输入密码时,文本框中的内容会被隐藏,以保护用户的隐私;3、数字文本框等等。

391

2023.10.12

vlookup函数使用大全
vlookup函数使用大全

本专题整合了vlookup函数相关 教程,阅读专题下面的文章了解更多详细内容。

28

2025.12.30

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Node.js 教程
Node.js 教程

共57课时 | 7.7万人学习

CSS3 教程
CSS3 教程

共18课时 | 4.1万人学习

Vue 教程
Vue 教程

共42课时 | 5.7万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号