控制请求并发数量

5 阅读4分钟

一、核心需求与场景分析

当需要发送多个请求(如8个),但需限制同时并行的请求数量(如2个),核心目标是:

  • 避免因并发请求过多导致浏览器性能下降或服务端压力过大;
  • 保证请求按合理顺序处理,提升用户体验。

二、实现方案:基于Promise的并发控制

方案1:手动管理请求队列(ES6+原生实现)

通过维护待发送队列和进行中请求集合,逐步发送请求。

function sendRequests(urls) {
  const maxConcurrent = 2; // 最大并发数
  const results = [];       // 存储所有请求结果
  let pendingCount = 0;     // 进行中请求数
  const queue = [...urls];   // 待发送队列
  
  // 发送请求的核心函数
  function sendNext() {
    // 若队列为空且无进行中请求,结束
    if (queue.length === 0 && pendingCount === 0) {
      return Promise.resolve(results);
    }
    
    // 当进行中请求数小于最大并发数时,发送新请求
    while (pendingCount < maxConcurrent && queue.length > 0) {
      pendingCount++;
      const url = queue.shift();
      
      results[urls.indexOf(url)] = fetch(url)
        .then(res => res.json())
        .then(data => {
          console.log(`请求 ${url} 完成`);
          return data;
        })
        .catch(error => {
          console.error(`请求 ${url} 失败`, error);
          return error;
        })
        .finally(() => {
          pendingCount--; // 请求完成后减少计数
          sendNext();      // 继续发送下一个请求
        });
    }
    
    return Promise.all(results); // 返回所有请求结果
  }
  
  return sendNext();
}

// 使用示例
const urls = [
  'https://api1.com', 'https://api2.com', 
  'https://api3.com', 'https://api4.com',
  'https://api5.com', 'https://api6.com',
  'https://api7.com', 'https://api8.com'
];

sendRequests(urls).then(results => {
  console.log('所有请求完成', results);
});
方案2:使用async/await + 队列生成器(更简洁的ES7实现)

通过生成器函数管理请求队列,配合async/await处理异步逻辑。

async function sendRequests(urls, maxConcurrent = 2) {
  const queue = [...urls];
  const results = new Array(urls.length).fill(null);
  let pending = 0;
  
  // 生成器函数:逐个获取待发送的URL
  function* createQueue() {
    while (queue.length > 0) {
      yield queue.shift();
    }
  }
  
  const requestQueue = createQueue();
  const sendRequest = async () => {
    if (pending >= maxConcurrent) {
      // 等待当前请求完成后再继续
      await Promise.resolve();
    }
    
    const { value: url } = requestQueue.next();
    if (url === undefined) return; // 队列为空时返回
    
    pending++;
    const index = urls.indexOf(url);
    
    try {
      const res = await fetch(url);
      results[index] = await res.json();
      console.log(`请求 ${url} 完成`);
    } catch (error) {
      results[index] = error;
      console.error(`请求 ${url} 失败`, error);
    } finally {
      pending--;
      await sendRequest(); // 递归发送下一个请求
    }
  };
  
  // 启动多个并发请求
  const starters = Array(maxConcurrent).fill().map(sendRequest);
  return Promise.all(starters).then(() => results);
}
方案3:使用RxJS操作符(响应式编程实现)

通过RxJS的concatMapmergeMap控制并发数。

import { from, of } from 'rxjs';
import { mergeMap, toArray } from 'rxjs/operators';

function sendRequests(urls, maxConcurrent = 2) {
  return from(urls).pipe(
    // mergeMap控制并发数,超过maxConcurrent的请求进入队列等待
    mergeMap(url => fetch(url).then(res => res.json()), maxConcurrent),
    toArray() // 收集所有结果
  ).toPromise();
}
方案4:封装并发控制工具函数(可复用)

将并发控制逻辑抽象为通用函数,支持任意异步任务。

/**
 * 控制并发数量的通用函数
 * @param tasks 异步任务数组(如() => fetch())
 * @param maxConcurrent 最大并发数
 */
async function controlConcurrency(tasks, maxConcurrent) {
  const results = new Array(tasks.length).fill(null);
  let pending = 0;
  let completed = 0;
  
  async function runTask(index, task) {
    pending++;
    try {
      results[index] = await task();
    } catch (error) {
      results[index] = error;
    } finally {
      pending--;
      completed++;
      // 发送下一个任务(如果有)
      if (completed < tasks.length && pending < maxConcurrent) {
        runTask(completed, tasks[completed]);
      }
    }
  }
  
  // 启动初始并发任务
  for (let i = 0; i < Math.min(maxConcurrent, tasks.length); i++) {
    runTask(i, tasks[i]);
  }
  
  // 等待所有任务完成
  await new Promise(resolve => {
    const interval = setInterval(() => {
      if (completed === tasks.length) {
        clearInterval(interval);
        resolve(results);
      }
    }, 100);
  });
  
  return results;
}

// 使用示例
const urls = [
  () => fetch('https://api1.com').then(res => res.json()),
  () => fetch('https://api2.com').then(res => res.json()),
  // ...其他请求函数
];

controlConcurrency(urls, 2).then(results => {
  console.log('所有请求完成', results);
});

三、核心实现原理与关键步骤

  1. 请求队列管理
    • 将所有请求存入队列,按顺序等待发送。
  2. 并发数控制
    • 通过计数器pendingCount限制同时进行的请求数量。
  3. 请求调度
    • 当有请求完成时,从队列中取出下一个请求发送(递归或循环实现)。
  4. 结果收集
    • 确保结果按原始顺序返回(通过索引匹配)。

四、问题

1. 问:为什么需要控制请求并发数?
    • 浏览器对同一域名的并发请求数有限制(如Chrome默认6个),超出后请求会排队;
    • 控制并发数可避免因请求过多导致的资源竞争、服务端过载或客户端性能下降。
2. 问:如何处理请求失败后的重试?
    • 在请求处理函数中添加重试逻辑(如最多重试3次):
    async function fetchWithRetry(url, retries = 3) {
      for (let i = 0; i < retries; i++) {
        try {
          return await fetch(url);
        } catch (error) {
          if (i === retries - 1) throw error;
          console.log(`重试请求 ${url} (第${i+1}次)`);
        }
      }
    }
    
3. 问:请求并发控制与请求优先级如何结合?
    • 将请求按优先级排序(如重要接口优先),优先发送高优先级请求;
    • 使用不同的并发数配置(如高优先级请求并发数为2,低优先级为1)。