如何限制大量请求并发

前言:

1、主流浏览器在 HTTP/1.1 下对同一域名的最大并发请求数通常是 6~8 个。超过限制的请求会进入队列,等待空闲的连接。

2、可以利用Promise模拟任务队列,控制并发请求数量,以避免对服务器造成过大的压力。(先进先出)

代码

一个简单的请求队列调度器,用于控制并发请求的最大数量

js 复制代码
import axios from 'axios'

export const handQueue = (
  reqs // 请求总数
) => {
  reqs = reqs || []
  
  const requestQueue = (concurrency) => {
    concurrency = concurrency || 6 // 最大并发数
    const queue = [] // 请求池
    let current = 0
    
    const dequeue = () => {
      while (current < concurrency && queue.length) {
        current++;
        const requestPromiseFactory = queue.shift() // 出列
        requestPromiseFactory()
          .then(() => { // 成功的请求逻辑
          })
          .catch(error => { // 失败
            console.log(error)
          })
          .finally(() => {
            current--
            dequeue()
          });
      }

    }

    return (requestPromiseFactory) => {
      queue.push(requestPromiseFactory) // 入队
      dequeue()
    }

  }

  const enqueue = requestQueue(6)

  for (let i = 0; i < reqs.length; i++) {

    enqueue(() => axios.get('/api/test' + i))
  }
}

功能拆解

  1. handQueue 函数:

    参数 reqs: 是一个数组,包含需要发送的请求。

    函数的主要目的是对这些请求进行队列管理,确保并发请求的数量不会超过设定的上限。

  2. requestQueue 函数:

    concurrency:最大并发数

    enqueue:用于将新的请求添加到队列并处理它们

    queue: 请求池,用于存储待处理的请求。

    current: 当前正在执行的请求数。

  3. dequeue 函数:

    复制代码
    1、从请求池中取出请求并发送。它在一个循环中运行,直到当前并发请求数current达到最大并发数concurrency或请求池queue为空。
    2、对于每个出队的请求,它首先增加current的值,然后调用请求函数requestPromiseFactory来发送请求。
    3、当请求完成(无论成功还是失败)后,它会减少current的值并再次调用dequeue,以便处理下一个请求。
    js 复制代码
    const dequeue = () => {  
      while (current < concurrency && queue.length) {  
        current++;  
        const requestPromiseFactory = queue.shift() // 出列  
        requestPromiseFactory()  
          .then(() => { // 成功的请求逻辑  
          })  
          .catch(error => { // 失败  
            console.log(error)  
          })  
          .finally(() => {  
            current--  
            dequeue()  
          });  
      }  
    }
    js 复制代码
    // 定义返回请求入队函数
    return (requestPromiseFactory) => {  
      queue.push(requestPromiseFactory) // 入队  
      dequeue()  
    }
    // 函数返回一个函数,这个函数接受一个参数requestPromiseFactory,表示一个返回Promise的请求工厂函数。
    // 这个返回的函数将请求工厂函数加入请求池queue,并调用dequeue来尝试发送新的请求,
    // 当然也可以自定义axios,利用Promise.all统一处理返回后的结果。

    使用一个 while 循环,当当前请求数 current 小于最大并发数 concurrency 并且队列 queue 中有待处理请求时:

    1、从队列中取出第一个请求工厂 requestPromiseFactory。

    2、调用 requestPromiseFactory() 开始请求。

    3、使用 then 和 catch 处理请求成功和失败的逻辑。

    4、在 finally 中,减少当前请求数 current,并递归调用 dequeue,以确保队列持续运行。

  4. 分发逻辑:

    for 循环遍历传入的请求数组 reqs。

    对于每个请求,通过 enqueue 将其加入队列。

    每个请求通过工厂函数 () => axios.get('/api/test' + i) 包装,保证每次调用都返回新的请求 Promise。

优化

  1. 超时管理:

    如果某个请求卡住,会阻塞队列,可为请求设置超时,例如:

    js 复制代码
    const timeout = (promise, ms) =>
      Promise.race([
        promise,
        new Promise((_, reject) => setTimeout(() => reject('Timeout'), ms)),
      ]);
  2. 错误处理:

    重试或告警机制:

    js 复制代码
     const retry = (fn, retries = 3) =>
      fn().catch(err => (retries > 0 ? retry(fn, retries - 1) : Promise.reject(err)));
  3. 队列清空检测:

    在所有请求处理完成后,触发回调或事件通知:

    js 复制代码
    if (queue.length === 0 && current === 0) {
      console.log('All requests completed');
    }
  4. 优化后的代码

    js 复制代码
    import axios from "axios";
    /**
     * 处理并发请求队列
     * @param {Array<Function>} reqs 请求总数
     * @param {Object} options 配置选项
     * @param {number} options.concurrency 最大并发数
     * @param {number} options.timeout 请求超时时间 (ms)
     * @param {Function} options.onQueueEmpty 队列清空后的回调
     */
    export const handQueue = (reqs, options = {}) => {
      const { concurrency = 6, timeout = 10000, onQueueEmpty = () => {} } = options;
    
      // 确保请求队列为数组
      reqs = reqs || [];
      const queue = [];
      let current = 0;
    
      // 超时管理器
      const timeoutRequest = (requestPromise, timeout) =>
        new Promise((resolve, reject) => {
          const timer = setTimeout(() => reject(new Error("Request timeout")), timeout);
          requestPromise
            .then((res) => {
              clearTimeout(timer);
              resolve(res);
            })
            .catch((err) => {
              clearTimeout(timer);
              reject(err);
            });
        });
    
      // 出列处理
      const dequeue = async () => {
        while (current < concurrency && queue.length) {
          current++;
          const requestPromiseFactory = queue.shift(); // 出列
    
          try {
            await timeoutRequest(requestPromiseFactory(), timeout); // 包装超时处理
            console.log("Request success");
          } catch (error) {
            console.error("Request failed:", error.message); // 错误处理
          } finally {
            current--;
            dequeue();
    
            // 如果队列为空且没有正在处理的请求,调用队列清空回调
            if (queue.length === 0 && current === 0) {
              onQueueEmpty();
            }
          }
        }
      };
    
      // 入队函数
      const enqueue = (requestPromiseFactory) => {
        queue.push(requestPromiseFactory);
        dequeue();
      };
    
      // 将请求添加到队列
      for (let i = 0; i < reqs.length; i++) {
        enqueue(() => axios.get(`/api/test${i}`));
      }
    };
  5. 使用

    js 复制代码
    const reqs = Array.from({ length: 20 }, (_, i) => () => axios.get(`/api/test${i}`));
    handQueue(reqs, {
     concurrency: 4, // 最大并发数
     timeout: 5000,  // 超时时间 5 秒
     onQueueEmpty: () => {
       console.log("All requests have been processed!");
     },
    });
相关推荐
Tipriest_1 分钟前
gem & rbenv介绍【前端扫盲】
前端·ruby·gem·rbenv·bundler
稀里糊涂的全栈5 分钟前
腾讯位置服务多边形绘制、编辑、删除
前端·javascript·vue.js
练习两年半的工程师3 小时前
使用React和google gemini api 打造一个google gemini应用
javascript·人工智能·react.js
勘察加熊人4 小时前
angular九宫格ui
javascript·ui·angular.js
姑苏洛言5 小时前
30天搭建消防安全培训小程序
前端
左钦杨6 小时前
Nuxt2 vue 给特定的页面 body 设置 background 不影响其他页面
前端·javascript·vue.js
yechaoa6 小时前
【揭秘大厂】技术专项落地全流程
android·前端·后端
MurphyChen6 小时前
🤯 一行代码,优雅的终结 React Context 嵌套地狱!
前端·react.js
逛逛GitHub6 小时前
推荐 10 个受欢迎的 OCR 开源项目
前端·后端·github
_xaboy7 小时前
开源 FormCreate 表单设计器配置组件的多语言
前端·vue.js·低代码·开源·可视化表单设计器