关于前端:如何实现并发请求限制🚫(并发限制高可用方案)

3,923 阅读9分钟

前言

前两天我的同事告诉我一个困扰着他的问题,就是低代码平台中存在很多模块,这些模块的渲染是由模块自身处理的,简言之就是组件请求了自己的数据,一个两个模块还好,要是一次请求了几十个模块,就会出现请求阻塞的问题,而且模块的请求都特别大。

大量的并发请求会导致网络拥塞和带宽限制。特别是当网络带宽有限时,同时发送大量请求可能会导致请求之间的竞争,从而导致请求的响应时间延长。

因此模块的加载就很不顺畅。。。

为了解决这个问题我设计了一个关于前端实现并发请求限制的方案,下面将详细解释这个并发请求限制的方案及实现源码。

核心思路及简易实现

一、收集需要并发的接口列表,并发数,接口调用函数。

二、遍历arr,对于每一项,创建一个promise实例存储到resArr中,创建的时候就已经开始执行了。

三、将创建的promise传入的实例数组中,对于每一项的promise设置其then操作,并将其存储到running数组中,作为执行中的标识。

四、当then操作触发之后则将running中的对应这一项删除,执行中的数组减一。

五、在遍历的回调函数最后判断当前是否超出阈值,当数量达到限制时开始批量执行,用await去处理异步,处理完一个即跳走,重新往running中注入新的实例。

async function asyncLimit(limitNum, arr, fn) {
  let resArr = []; // 所有promise实例
  let running = []; // 执行中的promise数组
  for (const item of arr) {
    const p = Promise.resolve(fn(item)); // 遍历arr,对于每一项,创建一个promise实例存储到resArr中,创建的时候就已经开始执行了
    resArr.push(p);
    if (arr.length >= limitNum) {
      // 对于每一项设置其then操作,并将其存储到running数组中,作为执行中的标识,当then操作触发之后则将running中的对应这一项删除,执行中的数组减一
      const e = p.then(() => running.splice(running.indexOf(e), 1));
      running.push(e);
      if (running.length >= limitNum) {
        // 当数量达到限制时开始批量执行,处理完一个即跳走,重新往running中注入新的实例
        await Promise.race(running);
      }
    }
  }
  return Promise.allSettled(resArr);
}

用例:

fn = (item) => { 
    return new Promise((resolve) => { 
        console.log("开始",item); 
        setTimeout(() => { console.log("结束", item); 
        resolve(item); 
     }, item) }); 
}; 
asyncLimit(2, [1000, 2000, 5000, 2000, 3000], fn)

注:但是这里的实现太过简陋,在真正的业务场景中往往没有这样使用场景,因此我对着段代码封装成一个符合前端使用的并发限制模块,下面是完整可用的代码实现

完整实现及源码用例

源码实现

首先,让我们来看一下代码及用例:

class AsyncExecutor { 
    constructor(limit, defaultFn) { 
      this.limitNum = limit; 
      this.defaultFn = defaultFn; 
      this.initTask()
    } 
    asyncLimit() {
        const task = this.promiseTaskQueue.shift(); 
        if(task) {
          const p = task()
          const e = p.then(() => {
              this.runningPromise.splice(this.runningPromise.indexOf(e), 1)
              if(this.promiseTaskQueue.length === 0) {
                  this.initTask()
              }
          });
          this.runningPromise.push(e);
          if (this.runningPromise.length >= this.limitNum) {
              this.racePromise = Promise.race(this.runningPromise); // 竞速promise
          }
        }
      }
    asyncExecute(item) { 
      this.promiseTaskQueue.push(this.getTask(item))
      if (this.racePromise) { 
        this.racePromise =  this.racePromise.then(async () => {
            this.asyncLimit(); 
            await Promise.race(this.runningPromise); 
          }) 
      } else { 
        this.asyncLimit(); 
      }
    }
    
    initTask() {
      this.promiseTaskQueue = [] // 真实执行promise集合 
      this.runningPromise = []; // 执行中的promise集合 
      this.racePromise = ''; // 竞速promise,标志进入
    }

    getTask(item) {
        return () => new Promise(resolve => {
            resolve((item.fn || this.defaultFn)(item.value || item))
        })
    }
  }

这里提供了一个面向对象的并发限制类。

测试用例

用例一:

const executor = new AsyncExecutor(3, (item) => {
    return new Promise((resolve) => {
      console.log("开始", item);
      setTimeout(() => {
        console.log("结束", item);
        resolve(item);
      }, item);
    });
  });

executor.asyncExecute({ value: 1000 });
executor.asyncExecute({ value: 2000 });
executor.asyncExecute({ value: 5000 });
executor.asyncExecute({ value: 2000 });
executor.asyncExecute({ value: 3000 });

效果:

image.png

用例二:

const executor = new AsyncExecutor(3, (item) => {
    return new Promise((resolve) => {
      console.log("开始", item);
      setTimeout(() => {
        console.log("结束", item);
        resolve(item);
      }, item);
    });
  });

setTimeout(()=>{
    executor.asyncExecute({ value: 3000 });
}, 300)
executor.asyncExecute({ value: 1000 });
executor.asyncExecute({ value: 2000 });
executor.asyncExecute({ value: 5000 });

效果:

image.png

用例三:

const executor = new AsyncExecutor(3, (item) => {
    return new Promise((resolve) => {
      console.log("开始", item);
      setTimeout(() => {
        console.log("结束", item);
        resolve(item);
      }, item);
    });
  });

executor.asyncExecute({ value: 1000 });
executor.asyncExecute({ value: 2000 });
executor.asyncExecute({ value: 5000 });
executor.asyncExecute({ value: 2000 });
executor.asyncExecute({ value: 3000 });
setTimeout(()=>{
    executor.asyncExecute({ value: 1000 });
}, 300)

效果:

image.png

用例四:

const executor = new AsyncExecutor(3, (item) => {
    return new Promise((resolve) => {
      console.log("开始", item);
      setTimeout(() => {
        console.log("结束", item);
        resolve(item);
      }, item);
    });
  });

executor.asyncExecute({ value: 2000 });
setTimeout(()=>{
    executor.asyncExecute({ value: 1000 });
}, 300)
executor.asyncExecute({ value: 5000 });
executor.asyncExecute({ value: 3000 });

效果:

image.png

用例五:

const executor = new AsyncExecutor(3, (item) => {
    return new Promise((resolve) => {
      console.log("开始", item);
      setTimeout(() => {
        console.log("结束", item);
        resolve(item);
      }, item);
    });
  });

executor.asyncExecute({ value: 2000 });
executor.asyncExecute({ value: 1000 });
executor.asyncExecute({ value: 1000 });
executor.asyncExecute({ value: 2000 });
setTimeout(()=>{
    executor.asyncExecute({ value: 1000 });
    executor.asyncExecute({ value: 5000 });
    executor.asyncExecute({ value: 3000 });
}, 5000)

效果:

image.png

代码解释

这段代码是一个异步执行器的类(AsyncExecutor),用于控制并发执行异步任务。下面是对代码的逐行解释:

构造函数

  • constructor(limit, defaultFn) :构造函数,接收并发限制数(limit)和默认处理函数(defaultFn)作为参数。
  • this.limitNum = limit; :将并发限制数赋值给实例属性 limitNum
  • this.defaultFn = defaultFn; :将默认处理函数赋值给实例属性 defaultFn
  • 调用initTask初始化并发限制相关变量。

asyncLimit

asyncLimit 方法用于执行异步任务并控制并发数。它的主要作用是从任务队列中取出一个任务,并将任务转化为 Promise 实例进行执行。具体步骤如下:

  1. 从任务队列 promiseTaskQueue 中取出第一个任务 task
  2. 如果存在任务,则执行以下操作:
  • 执行任务 task 并返回一个 Promise 实例 p
  • 创建一个完成后从正在执行的 Promise 数组中移除自身的 Promise 实例 —— e ,并且当任务全部执行完毕之后会重新初始化并发限制相关变量,保证下次运行正常。
  • 将 Promise 实例 e 添加到正在执行的 Promise 数组 runningPromise 中,runningPromise表示正在执行中的Promise集合
  • 如果正在执行的 Promise 数组长度达到并发限制数 limitNum ,则创建一个竞速 Promise racePromise ,用于控制并发数。
  • racePromise 是一个由Promise.race创建的promise,接收了 runningPromiserunningPromise存储了调用完之后会删除自身的promise,此时标识当前 AsyncExecutor 进入了限制状态。

asyncExecute

asyncExecute 方法是业务侧用于调用并发控制的函数。它的主要作用是将任务添加到任务队列中,并根据当前是否存在竞速 Promise 进行相应的处理。具体步骤如下:

  1. 将任务函数 this.getTask(item) 添加到任务队列 promiseTaskQueue 中,asyncLimit 可以取出来执行。
  2. 如果存在竞速 Promise racePromise ,则执行以下操作:
  • 使用 then 方法等待竞速 Promise完成后执行下一步操作。
  • 在竞速 Promise 完成后,意味着被限制的n个执行中的promise已经出现执行结束的了,最大并发数中出现了一个空位
  • 竞速 Promisethen 中重新调用 this.asyncLimit() 方法,从任务队列中取出新任务并执行,新的任务会加到 runningPromise 生成新的执行promise队列,所以后续所有的任务执行都得在 新的执行promise队列 执行完之后的 then 中执行
  • 下一步就要用 await 去等待 新的执行promise队列 执行完毕,即:
    await Promise.race(this.runningPromise);
  • 假如在队列达到了限制的情况下,又同时加入n个任务,该怎么保证每个任务都会在自己的 runningPromise 之后的 then 中执行呢?
  • 每一次执行asyncExecute即执行任务时,先判断当前是否存在 racePromise,如果存在 racePromise那么把执行函数放到的then中,并重置racePromise为设置thenracePromise,即:
this.racePromise =  this.racePromise.then(async () => {  
     this.asyncLimit();   
     await Promise.race(this.runningPromise);   
   })
  • 这样后续无论怎么调用,其任务执行函数都会在 上一个的 竞速 promise 的 then 中执行,并且then通过await去等待 新的竞速 promise 的结束,这样就实现了并发控制。
  1. 如果不存在竞速 Promise,则直接调用 this.asyncLimit() 方法执行异步限制。

getTask

根据传入的 item,返回一个获取任务的函数。

  • 调用后,它会执行传入的处理函数或默认处理函数 并返回一个Promise。

initTask

初始化并发限制相关变量

  • this.promiseTaskQueue = [] :存储执行中和执行过的 Promise 任务的数组。
  • this.runningPromise = [] :存储正在执行的 Promise 的数组。
  • this.racePromise = '' :标志进入竞速状态的 Promise,开始并发限制。

总结

使用上来说,需要先用AsyncExecutor类创建一个并发限制🚫的实例, 使用到接口的时候,调用实例的asyncExecutepromiseTaskQueue 加数据就行,默认会直接执行 asyncLimit 并根据情况创建一个promise链。

并发限制的原理主要是通过阻塞 racePromise 即竞速 promise实现的,当并发达到限制数之后就创建一个 racePromise,后续的asyncLimit 任务执行都要在 racePromisethen 中完成,当执行then时,会执行asyncLimit 任务,完成之后能得到新的racePromise,只有新的racePromise结束了这个then才能结束。

而后面的每一次的asyncExecute执行都会判断当前的 racePromise 是否存在 ,如果存在那么这次的执行就要放到 racePromisethen中,并且将这个racePromise 赋值为 设置了then的新racePromise。这样就可以保证每一个任务执行都在上一个racePromisethen中,就形成一个promise链。

这样就可以实现并发请求限制🚫

结语

通过使用这个并发限制的机制,我们可以控制同时执行的异步操作数量,避免过多的请求对服务器造成压力,提高性能和用户体验。

文字的描述可能比较枯燥,读者可以去cv一下代码,并执行一下用例,看看效果并思考一下为什么会出现这种情况。

后续业务场景中如果遇到类似情况的话,可以有个解决思路。

后续:

新增返回 asyncExecute promise 能力

源码

class AsyncExecutor {
  constructor(limit, defaultFn) {
    this.limitNum = limit;
    this.defaultFn = defaultFn;
    this.initTask();
  }

  asyncLimit(resolve, reject) {
    const task = this.promiseTaskQueue.shift();
    if (task) {
      const p = task();
      const e = p.then((res) => {
        resolve(res);
        this.runningPromise.splice(this.runningPromise.indexOf(e), 1);
        if (this.promiseTaskQueue.length === 0) {
          this.initTask();
        }
      }).catch((err) => {
        reject(err);
      });
      this.runningPromise.push(e);
      if (this.runningPromise.length >= this.limitNum) {
        this.racePromise = Promise.race(this.runningPromise); // 竞速promise
      }
    }
  }

  asyncExecute(item) {
    // 返回新的pormise
    return new Promise((resolve, reject) => {
      this.promiseTaskQueue.push(this.getTask(item));
      if (this.racePromise) {
        this.racePromise = this.racePromise.then(async () => {
          this.asyncLimit(resolve, reject);
          await Promise.race(this.runningPromise);
        });
      } else {
        this.asyncLimit(resolve, reject);
      }
    });
  }

  initTask() {
    this.promiseTaskQueue = []; // 真实执行promise集合
    this.runningPromise = []; // 执行中的promise集合
    this.racePromise = ''; // 竞速promise,标志进入
  }

  getTask(item) {
    return () =>
      new Promise((resolve) => {
        resolve((item.fn || this.defaultFn)(item.value || item));
      });
  }
}

用例

const executor = new AsyncExecutor(3, (item) => {
    return new Promise((resolve) => {
      console.log("开始", item);
      setTimeout(() => {
        console.log("结束", item);
        resolve(item);
      }, item);
    });
  });

executor.asyncExecute({ value: 1000 });
executor.asyncExecute({ value: 2000 }).then(res=>{console.log(2333, res, 'xiaoshan')});
executor.asyncExecute({ value: 5000 });
executor.asyncExecute({ value: 2000 }).then(res=>{console.log(2333, res, 'xiaoshan')});
executor.asyncExecute({ value: 3000 }).then(res=>{console.log(2333, res, 'xiaoshan')});;

解释

asyncExecute 函数返回一个新 Promise,这个promise会接收自身任务的执行结果。
后续就可以直接在executor.asyncExecute({ ... }) 中直接获取代码结果。