前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >NestJS接口在并发场景下的表现

NestJS接口在并发场景下的表现

作者头像
韦东锏
发布2024-04-02 17:00:35
1380
发布2024-04-02 17:00:35
举报
文章被收录于专栏:Android码农Android码农

在开发NestJS的时候,就很好奇,当某个接口有并发请求的时候,表现是怎样的,接下来做下验证

JS代码层面的耗时

新建一个并发验证的接口,在controller上,定义一个简单的get接口

代码语言:javascript
复制
async sleep() {
    return new Promise((resolve) => setTimeout(resolve, 500))
}
async concurrentTest(): Promise<string> {
    requestNumber++
    console.log('before request', requestNumber, Date.now())
    // 模拟一个接口请求,耗时500ms
    await this.sleep()
    this.logger.log('after request', requestNumber, Date.now())
    return 'success'
}

模拟下请求 http://localhost:3000/api/authorize/concurrentTest,接口返回正常,同时log也正常打印

代码语言:javascript
复制
before request 1 1709176114383
[Nest] 22430  - 02/29/2024, 11:08:34 AM     LOG after request
[Nest] 22430  - 02/29/2024, 11:08:34 AM     LOG 1
[Nest] 22430  - 02/29/2024, 11:08:34 AM     LOG 1709176114884

上面打印的时间戳,前后间隔有500ms,说明模拟接口的行为没问题

接下来,开始模拟并发的场景,看下结果

代码语言:javascript
复制
before request 454 1709175712455
before request 455 1709175712484
before request 456 1709175712588
before request 457 1709175712592
before request 458 1709175712639
before request 459 1709175712690
before request 460 1709175712783
before request 461 1709175712825
[Nest] 20717  - 02/29/2024, 11:01:52 AM     LOG after request
[Nest] 20717  - 02/29/2024, 11:01:52 AM     LOG 461
[Nest] 20717  - 02/29/2024, 11:01:52 AM     LOG 1709175712866
[Nest] 20717  - 02/29/2024, 11:01:52 AM     LOG after request
[Nest] 20717  - 02/29/2024, 11:01:52 AM     LOG 461
[Nest] 20717  - 02/29/2024, 11:01:52 AM     LOG 1709175712955
[Nest] 20717  - 02/29/2024, 11:01:52 AM     LOG after request
[Nest] 20717  - 02/29/2024, 11:01:52 AM     LOG 461
[Nest] 20717  - 02/29/2024, 11:01:52 AM     LOG 1709175712985
[Nest] 20717  - 02/29/2024, 11:01:53 AM     LOG after request
[Nest] 20717  - 02/29/2024, 11:01:53 AM     LOG 461
[Nest] 20717  - 02/29/2024, 11:01:53 AM     LOG 1709175713089
[Nest] 20717  - 02/29/2024, 11:01:53 AM     LOG after request
[Nest] 20717  - 02/29/2024, 11:01:53 AM     LOG 461
[Nest] 20717  - 02/29/2024, 11:01:53 AM     LOG 1709175713093
[Nest] 20717  - 02/29/2024, 11:01:53 AM     LOG after request
[Nest] 20717  - 02/29/2024, 11:01:53 AM     LOG 461
[Nest] 20717  - 02/29/2024, 11:01:53 AM     LOG 1709175713140

多个请求过来,接口是同时响应,同时处理,而且每个接口的耗时不会增多

总的处理,是在一个线程中处理的,上面的处理过程,其实就是JS的Event Loop机制和Microtasks机制

比如上面的concrrentTest方法,当碰到并发请求的时候,逻辑是这样

首先在JS浏览器,或者Node.JS中,有一个Event Loop的东西,事件循环负责执行代码和处理异步操作

当第一个请求进来,事件循环先处理了concrrentTest函数,执行了一个log,然后碰到await方法,函数被挂起,异步执行await后面的代码,任务队列继续执行下一个任务

event loop就继续处理第二个请求的concrrentTest函数,执行第二个请求的log方法,然后继续碰到await,第二次执行的函数继续被挂起,继续执行下一个task

当第一个函数的await任务执行完成后,它后续处理的函数会被放到microtasks queue中,event loop会首先处理所有的microtasks,然后再执行其他任务,所以await后续的逻辑被执行

以此类推

如果希望模拟在并发的时候,接口响应变慢,要如何模拟 我们可以新建一个耗时的方法,这个方法耗时0.5秒

代码语言:javascript
复制
timeConsumingTask() {
    let start = Date.now()
    let count = 0
    // 这个循环将会消耗一些时间,取决于循环的次数和操作的复杂性
    while (Date.now() - start < 500) {
        // .5秒钟的耗时操作
        count++
    }
    console.log(`耗时操作完成,循环了 ${count} 次`)
}

在每次接口调用的时候,都会执行这个耗时的方法

代码语言:javascript
复制
async concurrentTest(): Promise<string> {
    requestNumber++
    console.log('before request', requestNumber, Date.now())
    // 模拟一个接口请求,耗时500ms
    this.timeConsumingTask()
    this.logger.log('after request', requestNumber, Date.now())
    return 'success'
}

由于是单线程,当前一个请求由于方法耗时卡住了,后一个并发的请求只能等待,可以看下log

代码语言:javascript
复制
before request 124 1709188248140
耗时操作完成,循环了 5750365 次
[Nest] 43123  - 02/29/2024, 2:30:48 PM     LOG after request
[Nest] 43123  - 02/29/2024, 2:30:48 PM     LOG 124
[Nest] 43123  - 02/29/2024, 2:30:48 PM     LOG 1709188248640
before request 125 1709188248641
耗时操作完成,循环了 9159242 次
[Nest] 43123  - 02/29/2024, 2:30:49 PM     LOG after request
[Nest] 43123  - 02/29/2024, 2:30:49 PM     LOG 125
[Nest] 43123  - 02/29/2024, 2:30:49 PM     LOG 1709188249141
before request 126 1709188249141
耗时操作完成,循环了 9596828 次
[Nest] 43123  - 02/29/2024, 2:30:49 PM     LOG after request
[Nest] 43123  - 02/29/2024, 2:30:49 PM     LOG 126
[Nest] 43123  - 02/29/2024, 2:30:49 PM     LOG 1709188249641
before request 127 1709188249644
耗时操作完成,循环了 8478465 次
[Nest] 43123  - 02/29/2024, 2:30:50 PM     LOG after request
[Nest] 43123  - 02/29/2024, 2:30:50 PM     LOG 127
[Nest] 43123  - 02/29/2024, 2:30:50 PM     LOG 1709188250144

在并发请求的时候,平均接口的响应时间提升到了接近3秒

上面的是接口本身的js代码的耗时,下面继续验证下数据库的并发下的场景情况,项目内,使用的是Prisma ORM,分别验证三个场景的下的数据库表现

  • 数据库并发读
  • 数据库并发写
  • 数据库并发读写

数据库并发读

先用npx prisma studio命令,查看下目前的测试数据库的数据,截图如下

一共有7条数据,接下来新建查询数据库数据的接口

代码语言:javascript
复制
// controller
@Get('/find')
findChat() {
    return this.authorizeService.findFirst()
}
 
 
// service
// 查找第一个聊天记录
async findFirst() {
    const item = await this.prismaService.chat.findFirst()
    console.log('findFirst result', item)
    return item
}

第一次请求,耗时110ms,后续的请求耗时30ms左右,估计prisma内部有做了优化缓存

console也打印出查询结果的日志了

代码语言:javascript
复制
findFirst result {
  id: 1,
  wxUserId: 'YGP2126',
  prompt: 'string',
  createdAt: 2024-02-20T08:56:13.250Z,
  updatedAt: 2024-02-20T08:56:13.250Z
}

接下来,改造下接口,返回随机的任意一条数据,因为一共有7条数据,先这样写

代码语言:javascript
复制
// 查找任意随机一条聊天记录
async findFirst() {
    const random = Math.floor(Math.random() * 7) + 1
    const item = await this.prismaService.chat.findFirst({
        where: {
            id: random
        }
    })
    console.log('findFirst result', item)
    return item
}

接下来,开始模拟并发请求,看下耗时情况

平均响应时间28ms,单纯的并发读,并不会导致单个接口的请求时间变长

数据库并发写

新建一个写的接口

代码语言:javascript
复制
// constroller
@Get('/insert')
insertOne() {
    return this.authorizeService.insertOne()
}
 
 
// service
// 插入一条聊天记录
async insertOne() {
    const wxUserId = 'YGP0711'
    let prompt = '测试插入' + Math.floor(Math.random() * 100000)
    await this.prismaService.chat.create({
        data: {
            prompt,
            wxUserId
        }
    })
    console.log('insertOne', prompt)
    return prompt
}

验证调用,log打印正常

代码语言:javascript
复制
insertOne 测试插入2393

查看数据库,也确实插入了刚才的那条信息

单独调用接口,平均耗时80ms-300ms波动

接下来,验证并发调用写的场景

平均响应时间145ms,经验证,并发写并不会延长接口的耗时

为什么并发写不会延长接口的耗时,经了解,内部逻辑是这样的

连接池(Connection Pool): Prisma 使用连接池来管理与数据库的连接。这意味着,当你的应用程序需要与数据库交互时,它会从池中获取一个已经建立的连接,而不是每次都创建一个新的连接。这种方式可以显著提高性能,因为建立数据库连接是一个资源密集型的操作。 事件循环(Event Loop): 在 Node.js 环境中,Prisma 作为一个库运行在 Node.js 的事件循环中。Node.js 是单线程的,但它使用非阻塞 I/O 操作,这意味着数据库操作不会阻塞事件循环。相反,当数据库操作完成时,回调函数会被放入事件队列中,等待事件循环到达它们时执行。 当你发出一个请求给 Prisma(比如查询或更新数据),Prisma 会生成相应的 SQL 语句,并通过其连接池中的一个连接发送到数据库。数据库系统(MySQL )将在其自己的进程中执行这些查询,这通常涉及多线程,以优化查询的执行。

上面的第二点,是使用了JS的Event Loop和microtasks queue机制,保证所有await后续的逻辑,都可以被执行

数据库并发读写

首先把读的接口的随机改成1000,前面几次并发测试,已经累积很多数据了

代码语言:javascript
复制
// 查找任意随机一条聊天记录
async findFirst() {
    const random = Math.floor(Math.random() * 1000) + 1
    const item = await this.prismaService.chat.findFirst({
        where: {
            id: random
        }
    })
    console.log('findFirst result', item, random)
    return item
}

开始模拟并发读写

平均68ms,并发读写的接口效率也是很高

为什么读写也不会延长接口耗时,相关的解释,个人认为跟读的解释是一样的,不做赘述

至于为什么MySQL内部为什么可以高效的处理并发,了解了下 MySQL 是一个多线程的数据库管理系统,它使用多个线程来处理并发连接和查询。这里是 MySQL 在线程和进程方面的一些关键点:

  1. 多线程架构: MySQL 服务器运行为一个单一的进程,但在这个进程内部,它会创建多个线程来处理不同的任务。这种多线程架构允许 MySQL 高效地管理并发,因为每个连接都可以在自己的线程上运行,而不会影响其他连接。
  2. 连接线程: 当客户端程序连接到 MySQL 服务器时,服务器通常会为每个新的连接分配一个线程。这个线程被称为连接线程或会话线程。每个连接线程负责处理所有来自相应客户端的请求,并返回查询结果。
  3. 后台线程: 除了为每个客户端连接创建的线程之外,MySQL 还运行一些后台线程来处理各种管理任务,例如:
  • 主线程:负责管理其他线程,如分配和回收连接线程。
  • I/O线程:负责处理文件输入输出和网络通信。
  • SQL线程:在复制配置中,负责从主服务器接收和执行复制的操作。
  • 清理线程:负责清理不再需要的资源,如关闭非活跃的连接。
  1. 线程池: 在并发的环境下,创建和销毁大量线程可能会导致性能问题。因此,MySQL 提供了线程池插件,它可以限制服务器创建的线程数量,并重用线程来处理新的连接。这可以显著提高性能,特别是在需要处理大量短暂连接的应用场景。
  2. 锁定和并发控制: MySQL 使用锁定机制和多版本并发控制(MVCC,在 InnoDB 存储引擎中)来管理对数据库资源的并发访问。锁定可以防止数据冲突和不一致,而 MVCC 允许读取操作在不锁定资源的情况下进行,从而提并发性能。

我们这次的例子中,虽然读写并行,由于有MVCC机制,并不会触发数据库的锁定表或者锁定行,从而保持高效的操作

总结

第一次开发后端接口,难免担心,万一碰到高并发请求,接口会不会扛不住,这样一轮验证下来,基本可以放心了,框架跟底层库把很多逻辑都做的很好了,我们只是站在前人巨大的累积沉淀下,做一些微不足道的业务逻辑

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-04-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Android码农 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • JS代码层面的耗时
  • 数据库并发读
  • 数据库并发写
  • 数据库并发读写
  • 总结
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档