基于Promise.resolve实现Koa请求队列中间件

news2024/10/5 12:43:36

本文作者为360奇舞团前端工程师

前言

最近在做一个 AIGC 项目,后端基于 Koa2 实现。其中有一个需求就是调用兄弟业务线服务端 AIGC 能力生成图片。但由于目前兄弟业务线的 AIGC 项目也是处于测试阶段,能够提供的服务器资源有限,当并发请求资源无法满足时,会响应【服务器繁忙】,这样对于 C 端展示的我们是非常不友好的。基于当前的困境,第一想到的解决方案就是KafkaRabbitMQ,但实际上对于我们目前的用户体量来说,简直就是大材小用。于是转换思路,是不是可以利用js模拟队列的方式解决问题呢,答案是:可以,PromiseResolve 队列!

分析

Resolve 的理解

Promise 的核心用法就是利用 Resolve 函数做链式传递。例如:

new Promise(resolve => {
  resolve('ok')
}).then(res => {
  console.log(res)
})
// 输出结果:ok

通过上边的例子我们可以理解,ResolvePromise 对象的状态从 pending 变为 fullfilled ,在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。

核心点:异步

此时抛出一个问题:假如我把 resolve 回调函数都放入一个队列里,Promise 是不是一直处于pending 状态?pending 状态就意味着then函数一直处于 waitting 状态,直到队列中的 resolve 函数执行后,then 函数才能被执行?

制造阻塞的 Promise 函数

const queue = []
new Promise(resolve => {
  queue.push(resolve)
}).then(res => {
  console.log(res)
})
// 输出结果:Promise {<pending>}

queue[0]('ok')
// 输出结果:ok

为了佐证,直接贴图:

6f6ba4680028921155a502a9f4ab0f83.png
image.png

异步转同步

Koa2 属于洋葱模型,当请求过来以后需要调用 next 函数继续穿透,而我们的需求是限流,这意味着我们要阻塞请求,此时此刻,await举起了双手,阻塞这种不要脸的事我在行呀!

const queue = []
const fn = async = () => {
  await new Promise(resolve => {
    queue.push(resolve)
  })
  // ...一大波操作
}
// queue[0]()

如果 queue[0] 不执行,代码就会一直处于阻塞状态。那我们就可以利用await写一个中间件实现阻塞某些 api 的需求了。

// 阻塞所有请求,知道queue中的resolve函数被执行才会执行next
const queue = []
module.exports = function () {
  return async function (ctx, next) {
    await new Promise(resolve => {
      queue.push(resolve)
    })
    await next();
  };
};

实现中间件

原理和思路都捋直了,那就开搞吧。话不多说,贴代码:

const resolveMap = {};

/**
 * 请求队列
 * @param {*} ctx
 * @param {*} ifer 是否是图生图
 * @param {*} maxReqNumber 最大请求数量
 * @returns
 * @description
 * 使用promise解决请求队列问题
 * 1. 用于限制aicg的并发请求
 * 2. 当文生图是,根据风格分类存储resolve,当前请求响应完成时,触发消费队列中下一个请求
 * 3. 当图生图是,直接存储resolve到image风格,当前请求响应完成时,触发消费队列中下一个请求
 * 4. 同时处理的请求数量不超过maxReqNumber个,否则加入队列等待。
 */
function requestQueue(ctx, maxReqNumber) {
  const params = ctx.request.body ?? ctx.request.query ?? ctx.request.params ?? {};
  const style = params.style ?? 'pruned_cgfull';

  resolveMap[style] = resolveMap[style] || { list: [], processNumber: 0 };
  const currentResolve = resolveMap[style];

  ((currentResolve) => {
    ctx.res.on('close', () => {
      saveNumberMinus(currentResolve);
      // 当前请求响应完成时,触发消费队列中下一个请求
      if (currentResolve.list.length !== 0) {
        const node = currentResolve.list.shift();
        node.resolve();
        currentResolve.processNumber++;
      }
      currentResolve = null;
    });
  })(currentResolve);
  // 当前请求正在处理中,将resolve存储到队列中
  if (currentResolve.processNumber + 1 > maxReqNumber) {
    // 利用promise阻塞请求
    return new Promise((resolve, reject) => {
      // 当前请求正在处理中,将resolve存储到队列中
      currentResolve.list.push({ resolve, reject, timeStamp: Date.now(), params });
    });
  } else {
    currentResolve.processNumber++;
    return Promise.resolve();
  }
}

module.exports = function (options = {}) {
  const { maxReqNumber = 2, apis = [] } = options;
  return async function (ctx, next) {
    const url = ctx.url;
    if (apis.includes(url)) {
      try {
        await requestQueue(ctx, maxReqNumber);
      } catch (error) {
        console.log(error);
        ctx.body = {
          code: 0,
          msg: error,
        };
        return;
      }
    }
    await next();
  };
};

const fiveMinutes = 5 * 60 * 1000;
setInterval(() => {
  Object.values(resolveMap).forEach((item) => {
    const { timeStamp, resolve } = item;
    if (Date.now() - timeStamp > fiveMinutes) {
      resolve(); // 执行并释放请求,防止用户请求因异常积压导致一直挂起
      saveNumberMinus(item);
    }
  });
}, 5 * 60 * 1000);

这里要着重提示一点,闭包的使用。之所以使用闭包是为了保证当前请求的close事件触发时能够使用currentResolve对象。因为当前请求是放在自身对应风格的数组中,close时要消费下一个等待的请求,同时也不要忘了手动释放资源。

app.js 逻辑部分

const requsetQueue = require('./app/middleware/request-queue');
const app = new Koa();
app.use(
  requsetQueue({
    maxReqNumber: 1,
    apis: ['/api/aigc/image', '/api/aigc/textToImage', '/api/aigc/img2img'],
  })
);
app.listen(process.env.NODE_ENV === 'development' ? '9527' : '3000');

总结

其实基于 PromiseResolve 队列,我们还可以实现一些其他的功能,比如:前端代码中未登录状态下收集某些请求,等到登录成功后发送请求。也希望大家一起探索和讨论Promise的其他解决能力的实现方案。


- END -

关于奇舞团

奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。

0c94266f4114bc66f653a577a9889d38.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/861139.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

uniapp 扩展组件 uni-forms 的表单验证之 validateFunction 只响应一次

uniapp 扩展组件 uni-forms 的表单验证之 validateFunction 只响应一次 问题代码官方说明参考资料 问题代码 直接从官方示例中复制过来改的。为了演示 <template><view><uni-forms ref"form" :modelValue"formData" :rules"rules&qu…

vue-pc端实现按钮防抖处理-自定义指令

前言 我们经常在移动端会处理按钮和输入框的防抖和节流处理&#xff0c;在pc端很少进行这样的操作 但是在pc端也是可以进行按钮的防抖操作&#xff0c;这样也是比较合理&#xff0c;可以不用但不可以不会 我们只要配合vue项目自定义指令加上全局注册&#xff0c;就可以实现按…

maven的入门使用

maven的入门使用 1.Maven&#xff08;Maven Apache&#xff09;是一个流行的项目构建和管理工具&#xff0c;2.项目结构和POM文件&#xff1a;3.POM文件&#xff08;Project Object Model&#xff09;4.依赖管理&#xff1a; 在POM文件中5.生命周期和构建过程1.前言2.插件系统3…

【MAC】 M2 brew安装 docker 运行失败 解决

MAC 安装 brew install --cask docker 之后一直显示docker: Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?. 网上看了一些文章 发现 这个不适用于M2 所以要从官网上下载 docker 安装成功

Java算法_ 房子强盗(LeetCode_Hot100)

题目描述&#xff1a;你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋。每间房内都藏有一定的现金&#xff0c;影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统&#xff0c;如果两间相邻的房屋在同一晚上被小偷闯入&#xff0c;系统会自动报警。 给定一个代表…

在 Windows 上安装 OpenCV – C++ / Python

在这篇博文中&#xff0c;我们将在 Windows 上安装适用于 C 和 Python 的 OpenCV。 C 安装是在自定义安装 exe 文件的帮助下完成的。而Python的安装是通过Anaconda完成的。 在 Windows 上安装 OpenCV – C / Python&#xff08;opencv官方Wndows上安装openCV- C/ Pthon 的链接…

多核异构处理器A核与M核通信过程

多核异构处理器是指集成了不同类型或架构的CPU的系统级芯片&#xff08;SoC&#xff09;。 例如&#xff0c;有些处理器同时包含了高性能的A核&#xff08;如Cortex-A&#xff09;和低功耗的M核&#xff08;如Cortex-M&#xff09;。 这样的设计可以让不同的CPU负责不同的任务…

vue 项目中 utils 中 js 文件早于 main.js 文件调用

vue项目中utils中js文件早于main.js文件调用

手势识别-手势音量控制(opencv)

本项目是使用了谷歌开源的框架mediapipe&#xff0c;里面有非常多的模型提供给我们使用&#xff0c;例如面部检测&#xff0c;身体检测&#xff0c;手部检测等。 代码需要用到opencv HandTraqckModule模块 mediapipe模块和一个音量控制模块 AndreMiras/pycaw: Python Core…

Excel革命,基于电子表格开发的新工具,不是Access和Power Fx

深谙其道 在日常工作中&#xff0c;Excel是许多人不可或缺的办公工具。 是微软的旗下产品&#xff0c;属于Microsoft 365套件中的一部分&#xff0c;强大的数据处理和计算功能&#xff0c;被普遍应用在全球各行各业的人群当中&#xff0c;是一款强大且普及的电子表格软件。 于…

WebDAV之π-Disk派盘+Joplin

Joplin是一个优秀的开源笔记,可以组织到笔记本中的大量笔记和文本编辑器中进行复制,标记和修改。支持Evernote的笔记直接导入到Joplin应用程序中。Joplin还支持各种云服务同步,包括Dropbox、OneDrive、WebDAV或文件系统,方便对其进行检查、备份和移动。该应用程序可用于Win…

创建CREATE_STAT_TABLE 统计信息表在达梦和oracle中的使用

达梦 创建CREATE_STAT_TABLE 统计信息表 PROCEDURE CREATE_STAT_TABLE ( STATOWN VARCHAR(128), STATTAB VARCHAR(128), TABLESPACE VARCHAR(128) DEFAULT NULL, GLOBAL_TEMPORARY BOOLEAN DEFAULT FALSE ); 创建普通表的对应系统表的列名字段包括以下&#xff1a; OWNER TABL…

NLP 时事和见解【2023】

一、说明 AI的新闻当然不是即时的&#xff0c;但作为趋势和苗头&#xff0c;我们不得不做出自己的决定。比如&#xff0c;一些软件的支持是否持续&#xff0c;哪些现成的软件将不再使用&#xff0c;等等。 图片来自中途 以下是NLPlanet为您选择的有关NLP和AI的每周文章&#x…

3. 软件需求规格说明书 系统设计

目录 1. 软件需求规格说明书 2. 系统设计 3. 技术选型 4. 数据库实体 4.1 数据库设计 4.1.1 数据库名 forum_db 4.1.2 用户表 t_user 4.1.3 板块表 t_board 4.1.4 帖子表 t_article 4.1.5 帖子回复表 t_article_reply 4.1.6 站内信表 t_message 4.2 SQL 脚本 4.3 …

最小生成树—Kruskal算法

什么是最小生成树&#xff1f; 首先&#xff0c;最小生成树一定数无向图&#xff0c;并且在不影响所有点都连通的情况下&#xff0c;所有边的权重加起来最小值是多少。 比如说&#xff1a;无向图abcp如下图所示&#xff0c;每条边权重也标记出来了。最小生成树就如右侧所示。 …

什么是DNS的缓存?

DNS 缓存是一个临时的数据库&#xff0c;存储在计算机或网络设备&#xff08;如路由器&#xff09;上&#xff0c;用于保存最近的 DNS 查询结果。这种缓存机制可以加速后续的相同查询&#xff0c;因为设备可以直接从缓存中提取先前的查询结果&#xff0c;而不需要再次到外部的 …

0基础学C#笔记08:插入排序法

文章目录 前言一、过程简单描述&#xff1a;二、代码总结 前言 我们在玩打牌的时候&#xff0c;你是怎么整理那些牌的呢&#xff1f;一种简单的方法就是一张一张的来&#xff0c;将每一张牌插入到其他已经有序的牌中的适当位置。当我们给无序数组做排序的时候&#xff0c;为了…

07-2_Qt 5.9 C++开发指南_二进制文件读写(stm和dat格式)

文章目录 1. 实例功能概述2. Qt预定义编码文件的读写2.1 保存为stm文件2.2 stm文件格式2.3 读取stm文件 3. 标准编码文件的读写3.1 保存为dat文件3.2 dat文件格式3.3 读取dat文件 4. 框架及源码4.1 可视化UI设计4.2 mainwindow.cpp 1. 实例功能概述 除了文本文件之外&#xff…

竞赛项目 深度学习的口罩佩戴检测 - opencv 卷积神经网络 机器视觉 深度学习

文章目录 0 简介1 课题背景&#x1f6a9; 2 口罩佩戴算法实现2.1 YOLO 模型概览2.2 YOLOv32.3 YOLO 口罩佩戴检测实现数据集 2.4 实现代码2.5 检测效果 3 口罩佩戴检测算法评价指标3.1 准确率&#xff08;Accuracy&#xff09;3.2 精确率(Precision)和召回率(Recall)3.3 平均精…

刷题笔记 day7

力扣 209 长度最小的子数组 解法&#xff1a;滑动指针&#xff08;对同向双指针区间内的数据处理&#xff09; 1&#xff09;先初始化 两个指针 left &#xff0c;right。 2&#xff09;右移指针right的同时使用sum记录指针right处的值&#xff0c;并判断sum的值是否满足要求&…