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

本文作者为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.hqwc.cn/news/68099.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【C++】开源:spdlog跨平台日志库配置使用

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 这篇文章主要介绍spdlog日志库配置使用。 无专精则不能成&#xff0c;无涉猎则不能通。——梁启超 欢迎来到我的博客&#xff0c;一起学习&#xff0c;共同进步。 喜欢的朋友可以关注一下&#xff0c;下…

idea格式化日志打印

Live Template 需要在Live Templates里面创建一个模板组为MyTemplate 触发时机选择java 1、创建一个loge log.error($content$,$params$); content groovyScript("def params _3.collect {【it {}】}.join(, ); return \" _1 . _2 () exception > (params…

C语言题目的多种解法分享 2之字符串左旋和补充题

前言 有的时候&#xff0c;这个系列专栏中的解法之间并无优劣&#xff0c;只是给大家提供不同的解题思路 我决定将代码实现的过程写成注释&#xff0c;方便大家直接找到对应的函数&#xff0c;只有需要补充说明的知识才会单拿出来强调 这个系列的文章会更的比较慢&#xff0…

Leetcode-每日一题【剑指 Offer 31. 栈的压入、弹出序列】

题目 输入两个整数序列&#xff0c;第一个序列表示栈的压入顺序&#xff0c;请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如&#xff0c;序列 {1,2,3,4,5} 是某栈的压栈序列&#xff0c;序列 {4,5,3,2,1} 是该压栈序列对应的一个弹出序列&#xf…

proteus结合keil-arm编译器构建STM32单片机项目进行仿真

proteus是可以直接创建设计图和源码的&#xff0c;但是源码编译它需要借助keil-arm编译器&#xff0c;也就是我们安装keil-mdk之后自带的编译器。 下面给出一个完整的示例&#xff0c;主要是做一个LED灯闪烁的效果。 新建工程指定路径&#xff0c;Schematic,PCB layout都选择默…

【数据结构】堆的实现,堆排序以及TOP-K问题

目录 1.堆的概念及结构 2.堆的实现 2.1初始化堆 2.2销毁堆 2.3取堆顶元素 2.4返回堆的大小 2.5判断是否为空 2.6打印堆 2.7插入元素 2.8堆的向上调整 2.9弹出元素 2.10堆的向下调整 3. 建堆时间复杂度 4. 堆的应用 4.1 堆排序 4.2 TOP-K问题 1.堆的概念及结构 …

STM32 cubemx CAN

接收用到的结构体如下&#xff1a;CAN概念&#xff1a; 全称Controller Area Network&#xff0c;是一种半双工&#xff0c;异步通讯。 物理层&#xff1a; 闭环&#xff1a;允许总线最长40m&#xff0c;最高速1Mbps&#xff0c;规定总线两端各有一个120Ω电阻&#xff0c;闭环…

CSS的引入方式有哪些?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 内联样式&#xff08;Inline Styles&#xff09;⭐ 内部样式表&#xff08;Internal Stylesheet&#xff09;⭐ 外部样式表&#xff08;External Stylesheet&#xff09;⭐ 导入样式表&#xff08;Import Stylesheet&#xff09;⭐ 写在最…

vue之动态表单(优化)

代码资源在这儿 ↑ vue之动态表单优化 vue2js动态表单优化vue3ts动态表单优化 vue2js动态表单优化 效果图 目录结构 五个文件的完整代码: 以下是App.vue <template><div><router-view></router-view><Formpage /></div> </templa…

【论文简介】PP-OCRv1-v4中文字符识别论文概述

相关论文 2009.PP-OCR: A Practical Ultra Lightweight OCR System 2109.PP-OCRv2: Bag of Tricks for Ultra Lightweight OCR System 2206.PP-OCRv3: More Attempts for the Improvement of Ultra Lightweight OCR System 2308.PP-OCRv4&#xff1a;目前代码已发布&#xff08…

OKCC在系统安全方面做了哪些措施?

语音通信行业&#xff0c;运营者普遍比较关心的问题是&#xff0c;运营风险如何控制&#xff1f;运营安全如何保证&#xff1f;OKCC呼叫中心又有那些风控措施来保证运营安全。 当前阶段&#xff0c;语音通信运营&#xff0c;最主要的风险主要包括以下几个方面&#xff1a; 一、…

取证--实操

2022年美亚杯个人赛 运用软件DB Browser for SQLite &#xff08;一款用于查看SQLlite数据库文件的浏览器工具&#xff09; 火眼&#xff0c;盘古石手机取证系统等 案件详情 于2022年10月&#xff0c;有市民因接获伪冒快递公司的电邮&#xff0c;不慎地于匪徒架设的假网站提…