前端队列请求设计

436 阅读3分钟

前端队列请求设计

前言

原文链接: plums-send-vvh.craft.me/HhzdNNnyZAe…

如果有大量并发请求情况下, 一股脑的将请求抛到服务器是很偷懒的做法

而且如果是调用三方服务, 通常会对qps进行限制

例如之前有业务需要调用三方的图片翻译功能, 但是服务商将接口qps限制到5

这个时候如果同一时间有超出5个以上的请求会导致 超出的部分一直是错误的状态

解法

首先是数据结构, 假设qps是5, 其实就相当于有一个办事厅一次只能接待5个人, 超出的就在外面排队

办事厅完成一个, 后面再进来一个, 这是很符合生活直觉的

这种一边进一边出的场景, 很适合使用队列来处理

class Queue {
    constructor() {
      this.elements = {};
      this.head = 0;
      this.tail = 0;
    }
    // 入队
    enqueue(element) {
      this.elements[this.tail] = element;
      this.tail++;
    }
    // 出队
    dequeue(callback) {
      const item = this.elements[this.head];
      if(callback) callback(item);
      delete this.elements[this.head];
      this.head++;
      return item;
    }
    // 队首
    peek() {
      return this.elements[this.head];
    }
    // 队列长度
    length() {
      return this.tail - this.head;
    }
    // 是否为空
    isEmpty() {
      return this.length() === 0;
    }
}

const queue = new Queue();
queue.enque({...})

以上是一个基础版的队列数据结构,

首先分析一下业务, 由于服务商限制, 前端请求一次只能发送5次因为多余的请求即使发出也会失败

当队列满员后续操作需要阻塞, 这就是前端的 异步任务队列 (可以搜索这个获取相关的知识)

本质的操作是 await + Promise 模拟的阻塞效果

class AsyncTaskQueue {
    constructor(maxConcurrentRequests) {
        this.maxConcurrentRequests = maxConcurrentRequests; // 最大并发数 qps
        this.queue = new Queue(); // 用于存储排队的请求
        this.currentRequests = 0; // 当前正在进行的请求数
        this.pendingRequests = 0; // 等待中的请求数
    }
    
    // 执行请求的函数
    async enqueue(requestFn) {
        // 如果当前请求数达到最大并发数,排队等待
        if (this.currentRequests >= this.maxConcurrentRequests) {
            // 增加排队请求计数
            this.pendingRequests++;
            console.log('达到并发限制,生产者等待');
            await new Promise(resolve => {
                this.queue.enqueue(resolve); // 将resolve函数存入队列,等待被唤醒
            });
        }
        // 开始请求
        this.currentRequests++;
        try {
            await requestFn({
              currentRequests: this.currentRequests,
              pendingRequests: this.pendingRequests
            }); // 执行请求
        } catch (error) {
            console.error("请求失败", error);
        } finally {
            // 请求完成,减少当前请求数
            this.currentRequests--;
            // 当所有请求完成时,打印队列为空信息
            if (this.pendingRequests === 0 && this.queue.length() === 0) {
                console.log("队列为空, 等待新的请求");
            }
            // 如果有排队的请求,唤醒下一个
            if (this.queue.length() > 0) {
                const resolve = this.queue.dequeue();
                resolve(); // 唤醒下一个排队的请求
                this.pendingRequests--;
            } 
        }
    }
}

// 测试用例
// 模拟异步请求函数
async function mockRequest(id, currentRequests , pendingRequests) {
  console.log(`\x1b[32m请求 ${id} 开始 , 正在进行的请求${currentRequests} 当前排队长度 ${pendingRequests} \x1b[0m`);  // Green for start (入队)
  await new Promise(resolve => setTimeout(resolve, 1000)); // 模拟请求延迟
  console.log(`\x1b[31m请求 ${id} 完成\x1b[0m`); // Red for complete (出队)
}

// 创建队列实例,最大并发数为 3
const taskQueue = new AsyncTaskQueue(3);

// 模拟 10 个请求
for (let i = 1; i <= 10; i++) {
  taskQueue.enqueue(({currentRequests , pendingRequests}) => mockRequest(i, currentRequests , pendingRequests));  // 添加任务到队列
}

以上这个队列可以实现异步任务队列

只需要设置好并发的数量, 然后向队列中push任务即可, 这个时候就不用考虑如何调度请求的问题, 在内部已经处理好了. 下次遇到类似的需求赶紧试一试吧

总结

数据结构用好 实现一些业务功能真的能起到立竿见影的效果