axios请求并发限制

4,874 阅读5分钟

背景

今天遇到了一个问题,打开一个页面后网站挂了.Network里查看,后台有一张图片请求一直在pending,打开此页面后的后续其他的请求均没有响应,但经过验证这并非后台的服务器挂了,没返回图片响应.因为直接关闭当前的tab页,新开一个tab页,其他页面还是可以正常加载的,服务器也都正常响应。只是这个浏览器标签好像卡死了.

此页面与其他页面的区别在于并发请求比较多。所以我怀疑是ajax请求数量超过了chrome的最大并发数6,导致chrome的标签页卡死。因此尝试限制ajax请求的并发数量,此文记录过程.

模拟并发请求

为此,我模拟了并发请求,并且对并发请求进行限制.

这是没有经过限制的并发请求瀑布图.可以看出原始的并发请求在chrome浏览器下最大并发数为6,其余请求将会放在队列中,上面的6个请求中结束了一个会从队列中取出一个进行请求.

var promiseArr = [];
for(let i = 0;i<12;i++){
	promiseArr.push(function(){
		return new Promise(function(resolve,reject){
			let xhr = new XMLHttpRequest();
			xhr.onreadystatechange = function(){
				if(xhr.readyState===4 && xhr.status===200){
					resolve(xhr.responseText)
				}
			}
			xhr.open('GET', 'https://hacker-news.firebaseio.com/v0/topstories.json', true)
			xhr.send();
		})
	}())
}
Promise.all(promiseArr);

chrome.png

图1.原始并发请求

我还在火狐浏览器下测试了一下,也是类似的结果。

firefox6.png

根据[1]提供的方法,把火狐浏览器的并发数修改成8以后,再次测试。

firefox8.png

对并发请求进行限制

我参考了这里的代码:不到50行代码实现一个能对请求并发数做限制的通用RequestDecorator

class RequestDecorator {
  constructor ({
    maxLimit = 5,
    requestApi,
    needChange2Promise,
  }) {
    this.maxLimit = maxLimit;
    // 一个虚假的队列,里面压入的是现生成的promise,用于在达到最大并发数时卡住不向下进行.之前的请求结果返回一个,才能count--,然后调用next函数,resolve队列里的一个promise,继续执行代码,发起新的请求.
    this.requestQueue = [];
    this.currentConcurrent = 0;
    this.requestApi = needChange2Promise ? pify(requestApi) : requestApi;
  }
  async request(...args) {
    
    if (this.currentConcurrent >= this.maxLimit) {
      await this.startBlocking();
    }
    try {
      this.currentConcurrent++;
      const result = await this.requestApi(...args);
      return Promise.resolve(result);
    } catch (err) {
      return Promise.reject(err);
    } finally {
      console.log('当前并发数:', this.currentConcurrent);
      this.currentConcurrent--;
      this.next();
    }
  }
 
  startBlocking() {
    let _resolve;
    let promise2 = new Promise((resolve, reject) => _resolve = resolve);
    this.requestQueue.push(_resolve);
    return promise2;
  }
 
  next() {
    if (this.requestQueue.length <= 0) return;
    const _resolve = this.requestQueue.shift();
    _resolve();
  }
}

module.exports = RequestDecorator;

完整代码如下:

<!DOCTYPE html>
<html>
<head>
	<title></title>
	<!--为了可以在ie上运行-->
	<script src="http://cdn.jsdelivr.net/bluebird/3.5.0/bluebird.min.js"></script>
</head>
<body>
<script type="text/javascript">
class RequestDecotration {
	constructor(max) {
		this.max = max;
		this.waitQueue = [];
		this.currentIndex = 0;
	}
	async request(caller, ...args) {
		if (this.currentIndex >= this.max) {
			await this.wait();
		}
		this.currentIndex++;
		try {
			const res = await caller();
			//返回一个thenable的promise对象。为了和axios返回的结果类型保持一致
			return Promise.resolve(res)
		} catch (error) {
			return Promise.reject(error)
		} finally {
			//经历过了return的竟然还能到finally。。。涨姿势了。
			this.currentIndex--;
			this.next();
		}
	}
	wait() {
		let tempResolve = null;
		let promise = new Promise((resolve, reject) => { tempResolve = resolve })
		this.waitQueue.push(tempResolve);
		return promise
	}
	next() {
		if (this.waitQueue.length > 0) {
			let _resolve = this.waitQueue.shift();
			_resolve();
		}
	}
}

var promiseArr = [];
for(let i = 0;i<12;i++){
	promiseArr.push(function(){
		return new Promise(function(resolve,reject){
			let xhr = new XMLHttpRequest();
			xhr.onreadystatechange = function(){
				if(xhr.readyState===4 && xhr.status===200){
					resolve(xhr.responseText)
				}
			}
			xhr.open('GET', 'https://hacker-news.firebaseio.com/v0/topstories.json', true)
			xhr.send();
		})
	})
}
//这里传入限并发数的参数
let requestDecotration = new RequestDecotration(3);
promiseArr.forEach(xhrPromise=>requestDecotration.request(xhrPromise));
</script>
</body>
</html>

5-2.png

图2.并发数为5

4.png

图3.并发数为4

3.png

图4.并发数为3

2.png

图5.并发数为2

真实页面的瀑布图

test.jpeg

上图是真实页面的请求瀑布图,限制最大并发数为2.可以看出这个代码其实并非完全每一次都并发2个,而是上2个请求中的一个请求结束了,会开始下一个请求.可以从画的竖线上看出来

test2.jpeg

上图依然设置了最大并发数为2,为什么看起来没有第一张图的并发看起来明显呢?如刚才说,因为上2个请求中的第二个请求一直没返回响应,所以要等第三个请求结束才能开始第四个请求,第四个请求结束开始第五个请求,结果就变成了一个一个请求的发起,直到第二个请求返回响应结果.

并发数4.png
上图是设置了最大并发数为4,可以看出来依然是这个原因导致的绿色的响应返回时间呈阶梯状.

不过我们也能看到限制了并发数后,最后一个图片semi-transparent请求依然是pending的状态,页面还是卡死的状态.所以此文使用限制并发数的方法对解决此场景下的问题没有帮助,不过也是值得记录学习的.

最后此问题的出现原因经过排查在于,后端返回图片是计算的content-length大小与实际图片大小不等,仔细看会发现实际请求中包含了3个图片请求.虽然图片在chrome上都显示出来了.但实际上请求并未结束,chrome下会提示: Caution: request is not finished yet

这在Chrome/edge/firefox等浏览器上图片都会正确显示,从network里看请求列表也不容易发现问题.只有点进去请求详情,点到time里,才能看出端倪.但是诚实的ie直接拒绝显示这3张图片,这才发现了问题.此处,感谢单纯没心机的ie.

ref

1浏览器 HTTP 并发请求规则探讨

2Promise.resolve()

3Understanding Resource Timing