基于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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Java算法_ LRU 缓存(LeetCode_Hot100)

题目描述&#xff1a;请你设计并实现一个满足 LRU &#xff08;最近最少使用&#xff09; 缓存 约束的数据结构。 获得更多&#xff1f;算法思路:代码文档&#xff0c;算法解析的私得。 运行效果 完整代码 import java.util.HashMap; import java.util.Map;/*** 2 * Author: L…

微信小程序备案流程

微信小程序备案流程 &#x1f4d4; 千寻简笔记介绍 千寻简笔记已开源&#xff0c;Gitee与GitHub搜索chihiro-notes&#xff0c;包含笔记源文件.md&#xff0c;以及PDF版本方便阅读&#xff0c;且是用了精美主题&#xff0c;阅读体验更佳&#xff0c;如果文章对你有帮助请帮我…

Oracle database Linux自建环境备份至远端服务器自定义保留天数

环境准备 linux下安装oracle 请看 oracle12c单节点部署 系统版本: CentOS 7 软件版本&#xff1a; Oracle12c 备份策略与实现方法 此次备份依赖Oracle自带命令exp与linux下crontab命令&#xff08;定时任务&#xff09; exp Oracle中exp命令是一个用于导出数据库数据和对象的…

算法竞赛入门【码蹄集新手村600题】(MT1140-1160)C语言

算法竞赛入门【码蹄集新手村600题】(MT1140-1160&#xff09;C语言 目录MT1141 数字3MT1142 整除的总数MT1143 沙哈德数MT1144 整除MT1145 全部整除MT1146 孙子歌诀MT1147 古人的剩余定理MT1148 隐晦余8MT1149 余数MT1150 战死四五百MT1151 韩信生气MT1152 韩信又生气了MT1153 …

UML类图

UML类图 类与类之间的关系 类与类之间的关系 依赖 一个类的对象,作为另一个类的局部变量, 虚线加箭头表示继承 实线三角实现 虚线三角关联 一个类的对象,作为一个类的字段 实线箭头 a. 组合 实心菱形实线箭头 b. 聚合 空心菱形实线箭头

甄品焕新|燕千云服务请求预警功能上线,燕小千AIGC能力再升级

​ 燕千云数智化业务服务平台发布了1.23.0版本&#xff0c;此次版本上线了服务请求预警功能&#xff0c;增加呼叫中心服务场景中的通话质检功能&#xff0c;提高了企业IT服务效率。此次还升级了燕小千AIGC能力&#xff0c;不仅可以实时预估文档学习时间&#xff0c;还可以一键分…

MySQL 存储过程、函数、触发器、事件

​ 目录 存储过程 创建存储过程 调用存储过程 查看存储过程 删除存储过程 进阶 变量 if条件判断 传递参数 case结构 while循环 repeat结构 loop语句 leave语句 游标/光标 存储函数 触发器 创建触发器 删除触发器 查看触发器 事件 查看事件调度器是否开启…

Nginx负载均衡(重点)

正向代理 部署正向代理 server { listen 80; server_name localhost; #charset koi8-r; #access_log logs/host.access.log main; location / { root html; index index.html index.htm; proxy_pass http://20.0.0.60:80…

新手如何快速学习单片机?

初步确定学习目标&#xff1a;是学习简单便宜的51呢&#xff0c;还是学习简单但是性价比已经不算太高的&#xff0c;但是功能强大稳定可靠的avr&#xff0c;还是物美价廉的stm32&#xff0c;或者ARM9&#xff08;可以跑系统了&#xff09;&#xff0c;再往上x86什么的如果是学8…

【Linux】UDP协议——传输层

目录 传输层 再谈端口号 端口号范围划分 认识知名端口号 两个问题 netstat与iostat pidof UDP协议 UDP协议格式 UDP协议的特点 面向数据报 UDP的缓冲区 UDP使用注意事项 基于UDP的应用层协议 传输层 在学习HTTP等应用层协议时&#xff0c;为了便于理解&#xff…

Word转PDF在线转换如何操作?分享转换技巧

现如今&#xff0c;pdf转换器已成为大家日常办公学习必不可少的工具&#xff0c;市场上的pdf转换器主要有两种类型&#xff0c;一种是需要下载安装的&#xff0c;另一种是网页版&#xff0c;打开就可以使用的&#xff0c;今天小编给大家推荐一个非常好用的网页版pdf转换器&…

json-server的入门

由于前端开发的时候&#xff0c;需要向后端请求数据&#xff0c;有的时候后端还没有准备好&#xff0c;所以需要使用一些简单的静态数据&#xff0c;但是我们更加希望能够模拟请求以及请求回来的过程&#xff0c;这个时候就需要使用json-server Json-Server的介绍 json-server…

bye 我的博客网站

Bye&#x1f64b;&#x1f64b;&#x1f64b;&#xff0c;我的博客网站。在我的服务器上运行了9个月之久的博客网站要和大家Bye了。 背景 可能很多人不知道我的这个博客网站的存在&#xff0c;好吧&#xff0c;最后一次展示它了&#xff0c;博客网站地址在这里&#xff0c;它…

【unity】ShaderGraph实现等高线和高程渐变设色

【unity】ShaderGraph实现等高线和高程渐变设色 等高线的实现思路 方法一&#xff1a; 通过Position节点得到顶点的高度&#xff08;y&#xff09;值&#xff0c;将高度值除去等高距离取余&#xff0c;设定余数的输出边界&#xff08;step&#xff09; 方法二&#xff1a; 将…

ElasticSearch详细操作

ElasticSearch搜索引擎详细操作以及概念 文章目录 ElasticSearch搜索引擎详细操作以及概念 1、_cat节点操作1.1、GET/_cat/nodes&#xff1a;查看所有节点1.2、GET/_cat/health&#xff1a;查看es健康状况1.3_、_GET/_cat/master&#xff1a;查看主节点1.4、GET/_cat/indices&a…

STM32F105RCT6 -- ST-Link ITM Trace printf 打印日志

1. STM32 可以配置UASRT&#xff0c;使用串口来打印日志&#xff0c;还有另外一种方式&#xff0c;使用ITM 调试功能来打印日志&#xff0c; 主要使用到的三个函数 core_cm3.h 1.1 发送函数 static __INLINE uint32_t ITM_SendChar(uint32_t ch)&#xff0c;相当于串口的发送函…

分享之python 协程

线程和进程的操作是由程序触发系统接口&#xff0c;最后的执行者是系统&#xff1b;协程的操作则是程序员。 协程存在的意义&#xff1a;对于多线程应用&#xff0c;CPU通过切片的方式来切换线程间的执行&#xff0c;线程切换时需要耗时&#xff08;保存状态&#xff0c;下次继…

CMU 15-445 -- Introduction to Distributed Databases - 19

CMU 15-445 -- Introduction to Distributed Databases - 19 引言System ArchitectureShared MemoryShared DiskShared Nothing Early Distributed Database SystemsDesign IssuesHomogeneous VS. Heterogeneous Database PartitioningNaive Table PartitioningHorizontal Part…

Grafana技术文档--基本安装-docker安装并挂载数据卷-《十分钟搭建》

阿丹&#xff1a; Prometheus技术文档--基本安装-docker安装并挂载数据卷-《十分钟搭建》_一单成的博客-CSDN博客 在正确安装了Prometheus之后开始使用并安装Grafana作为Prometheus的仪表盘。 一、拉取镜像 搜索可拉取版本 docker search Grafana拉取镜像 docker pull gra…

数字万用表测量基础知识--DMM的显示位数

概览 DMM&#xff08;即数字万用表&#xff09;是一种电气测试和测量仪器&#xff0c;可测量直流和交流信号的电压、电流和电阻。本文介绍如何正确使用和理解数字万用表(DMM)。 DMM的显示位数 数字万用表(DMM)可用于进行各种测量。在选择DMM或理解所使用的DMM时&#xff0c;首…