前端请求除了要控制并发,还要控制 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)

是不是很简单~

相关推荐
n***i955 分钟前
前端技术的下一场进化:从工程化走向智能化的全面重构
前端·重构
@大迁世界10 分钟前
紧急:React 19 和 Next.js 的 React 服务器组件存在关键漏洞
服务器·前端·javascript·react.js·前端框架
闲人编程10 分钟前
微服务API网关设计模式
python·缓存·微服务·设计模式·系统安全·api·codecapsule
晓得迷路了11 分钟前
栗子前端技术周刊第 109 期 - Vite 8 Beta、JavaScript 三十周年、Prettier 3.7...
前端·javascript·vite
Terry_Tsang12 分钟前
ceph mon 报错 full ratio(s) out of order 解决方法
服务器·前端·ceph
韩曙亮16 分钟前
【Web APIs】元素偏移量 offset 系列属性 ④ ( offset 属性案例 - 放大镜效果 )
前端·javascript·css·html·offset·dom·web apis
宁雨桥18 分钟前
前端网页加载进度条实现指南:Vue3+Vite工程化场景
前端·javascript·性能优化
Mike_jia21 分钟前
ZabbixWatch:打造现代化运维监控大屏,让数据掌控触手可及
前端
John_ToDebug24 分钟前
深入探索 Chrome 中渲染进程与浏览器进程之间的 Mojo IPC 通信机制
前端·chrome·mojo
m0_4711996326 分钟前
【JavaScript】forEach 和 map 核心区别(附示例+选型)
开发语言·前端·javascript