瓶子笔记

关于并发请求

2024-07-01 · 6 min read

日常开发中的并发请求,一般不会去特地控制,直接依次请求就好,或者使用 Promise.all() 包装一层,等待所有成功的结果。但是当并发请求的数量大起来后,如果不处理并发量,可能会造成较严重的后果。比如大文件上传的场景,一般大文件上传需要切片分传,如果文件较大,可能会被分成100个小块分别请求,那这种同时100条的并发请求,大概率会有以下几个问题:

  1. 浏览器会限制,首先浏览器会限制同一域名下的并发请求数量,100个请求是不会立刻并发执行的,会排队处理,可能导致部分请求等待较长时间才开始;
  2. 服务器压力,即使绕过了浏览器的并发请求数量,短时间内大量的请求也会给服务器带来较大压力;
  3. 网络拥堵,大量并发请求会占用较多的网络带宽,可能导致其他网络活动受到影响,影响用户体验。

所以我们需要对大并发请求做个控制,即限制每次并发的请求量,比如每次只请求5条,100条请求分20次完成。那么怎么实现这种效果呢?

我看好多人会用 Promise.all() 来处理,比如将100条异步请求分成20个数组,每个数组有5个请求,每次将一个请求数组传入 Promise.all()中,当这组数据请求完,再传入下一个数组,以此类推直至请求结束。这种做法在理论上确实可以解决并发问题,之所以是理论上,主要有两个问题:

  1. Promise.all() 不能保证所有的请求结束后再给出结束状态,一旦有一个请求错误,会直接返回错误信息,不管后面的成员还在不在请求中。这样本质上还是无法准确控制同时并发量;
  2. 即使每次传入的一组请求,都是成功状态返回,或者使用Promise.allSettled,保证所有的请求结束后才返回状态。那么还是有效率问题,因为它需要等待这组数据全部请求完才能开启下一轮,如果这一组数据有一个接口需要10秒,那么这组数据请求就需要多等10秒,显然这问题是很大的。

所以最终还是需要我们自己手动来封装一个并发请求控制,主要实现两个功能:

  1. 可以灵活控制并发数量;
  2. 请求应该是队列形式,当一组请求中的某一条请求结束了,应自动加入下一条请求,以保证最大的效率。

最后,根据需求,参考网上资料,我简单封装了一个并发请求方法,代码如下:

function concurRequest(urls, maxNum) {
	return new Promise((resolve) => {
		if (urls.length === 0) {
			resolve([])
		}
		// 请求的 url 索引
		let index = 0
		// 已完成请求的数量
		let count = 0
		// 请求到的结果,每项都是 promise
		const result = []
		// 请求函数
		async function request() {
			// 缓存原始的下标
			const i = index
			// 当前要请求的 url
			const url = urls[index]
			// 获取完当前的请求地址后,递增下标,为下一次请求准备
			index++
			try {
				const res = await fetch(url)
				result[i] = res
			} catch (err) {
				result[i] = err
			} finally {
				count++
				// 如果已请求的数量等于总请求数量,那么就返回结果
				if (count === urls.length) {
					resolve(result)
				}
				if (index < urls.length) {
					request()
				}
			}
		}

		// 第一次触发请求
		for (let i = 0; i < Math.min(maxNum, urls.length); i++) {
			request()
		}
	})
}

concurRequest 方法可以传入两个参数,接口请求路径数组urls和并发数量maxNum,方法会按照传入的最大请求数来控制同时并发量,当某一个请求结束后,会按照接口数组中的顺序自动请求下一条,直至结束。当然这只是一个范例,我们可以根据实际开发需求改动,比如可以传入一组定义好的请求方法,这样每个请求方法的内部都可以灵活控制。