前端请求除了要控制并发,还要控制 QPS

背景

之前写了一篇 《ChatGPT 给我的「万能的」控制 Promise 并发的方法》,里面控制了网络请求并发时的数量,也就是同一时刻,最多只能有 n 个请求发送。

值得一提的是 ChatGPT 给了我非常大的启发,它提供的这种信号量(Semaphore)的方式是我第一次了解(也可能是我孤陋寡闻),这种思路绝对是可以复用在很多其它地方的。

比如这次,在项目中虽然前端请求用了频控,但是后端接口规定 QPS < 15 才可以 。也就是说如果每个接口返回的都很快,即使用了频控,也很有可能在 1s 内发出 15 个以上的请求。所以在实际使用过程中,还是会出现接口报错。

参考信号量频控的思路,我们来修改一下逻辑就可以了。

分析

我们先来看下频控的使用方法:

ts 复制代码
// 自定义装饰器函数,用于限制并发请求数量
function limitConcurrency(fn, limit) {
  const semaphore = new Semaphore(limit);
  
  return async function(...args) {
    await semaphore.acquire(); // !!!!注意这里!!!!!
    
    try {
      return await fn.apply(this, args);
    } finally {
      semaphore.release();
    }
  };
}

// 使用装饰器包裹 fetch 请求
const limitedFetch = limitConcurrency(fetch, 5);

// 并发发起 20 个请求
const urls = ['url1', 'url2', 'url3', ...]; // 假设有 20 个请求
const promises = urls.map(url => limitedFetch(url));

Promise.all(promises)
  .then(responses => {
    // 处理所有请求的响应
  })
  .catch(error => {
    // 处理错误
  });

看过 《ChatGPT 给我的「万能的」控制 Promise 并发的方法》 文章的朋友可能注意到了,整个方法的关键就是在 await semaphore.acquire(); 这一句。没看过也不要紧,简单粗暴一点的解释,就是这个地方一直在阻塞着,等待着,不满足条件这里就进行不下去。好了,我们借着这个思路,来改造一下这里的逻辑就可以了。

首先得有计数和计时,它们的消费逻辑大概是:

  1. 如果 1s 以内的请求数小于 n,那就正常通过,不阻塞;
  2. 如果超过了 n,并且还没超过 1s,就无限循环阻塞,每次判断是否可以通过;
  3. 如果时间超过了 1s,就可以将计数清零,通过正在阻塞的请求;
  4. 重复 1~3 的逻辑。

我们直接上代码。

代码实现

ts 复制代码
class Semaphore {
  private limit: number;
  private count: number; // 计数
  private lastRunTime: number; // 计时
  private duration = 1000;

  constructor(limit: number) {
    this.limit = limit;
    this.count = 0;
    this.lastRunTime = Date.now();
  }

  public acquire() {
    // 每次发起都判断下是否超过了 1s,如果超过就更新下计数和计时
    if (Date.now() - this.lastRunTime > this.duration) {
      this.lastRunTime = Date.now();
      this.count = 0;
    }
    // 靠 Promise 控制 resolve 的触发来实现阻塞
    return new Promise<void>((resolve) => {
      // 计数未超过限制时正常执行
      if (this.count < this.limit) {
        this.count++;
        resolve();
      } else {
        // 计数超过限制后开始无限循环
        const timer = setInterval(() => {
          if (Date.now() - this.lastRunTime > this.duration) {
            // 直到超过 1s 后,更新计数和计时
            this.lastRunTime = Date.now();
            this.count = 0;
          }
          if (this.count < this.limit) {
            // 满足条件时,正常执行,跳出循环
            this.count++;
            resolve();
            clearInterval(timer);
          }
        }, 100);
      }
    });
  }
}

我们看到,这回不需要 release 方法来解除 acquire 的阻塞了。直接在 acquire 内部实现解除阻塞的逻辑就可以了。所以 Decorator 的代码稍微改动一下,去掉 release 就可以了,如下:

ts 复制代码
type AsyncFunction = (...args: any[]) => Promise<any>;

export const limitConcurrency = (
  fn: AsyncFunction,
  limit: number,
): AsyncFunction => {
  const semaphore = new Semaphore(limit);

  return async (...args): Promise<any> => {
    await semaphore.acquire();

    try {
      return await fn(...args);
    } catch (e) {
      console.error('limitConcurrency', e);
    }
    // 去掉了 semaphore.release();
  };
};

// 使用装饰器包裹 fetch 请求
const limitedFetch = limitConcurrency(fetch, 15);

到此代码就都完成了,使用的话把所有的 fetch 替换成 limitedFetch 就可以了。

总结

这种信号量的思路的确很精彩,但是其本质又非常的简单。就是在你原本的方法前面加了一个 Semaphore 阻塞的逻辑。

这个阻塞的逻辑又是内聚在 Semaphore 内部的,非常的解耦和易于维护,侵入性极小。强烈推荐掌握这种思路。

大胆一点,如果既想要频控,又想要控 QPS,把 2 个 Decorator 同时使用就可以了。

ts 复制代码
const limitedFetch = limitCount(limitQps(fetch, 15), 5)

是不是很简单~

相关推荐
Leyla3 分钟前
【代码重构】好的重构与坏的重构
前端
影子落人间7 分钟前
已解决npm ERR! request to https://registry.npm.taobao.org/@vant%2farea-data failed
前端·npm·node.js
世俗ˊ31 分钟前
CSS入门笔记
前端·css·笔记
子非鱼92131 分钟前
【前端】ES6:Set与Map
前端·javascript·es6
6230_36 分钟前
git使用“保姆级”教程1——简介及配置项设置
前端·git·学习·html·web3·学习方法·改行学it
想退休的搬砖人1 小时前
vue选项式写法项目案例(购物车)
前端·javascript·vue.js
加勒比海涛1 小时前
HTML 揭秘:HTML 编码快速入门
前端·html
啥子花道1 小时前
Vue3.4 中 v-model 双向数据绑定新玩法详解
前端·javascript·vue.js
麒麟而非淇淋1 小时前
AJAX 入门 day3
前端·javascript·ajax
战神刘玉栋1 小时前
《程序猿之设计模式实战 · 观察者模式》
python·观察者模式·设计模式