You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Express.js如何实现多请求并行执行不阻塞

问题根因

阻塞不是Node.js天生无法处理并发,是现有代码存在致命缺陷,加上长查询场景的不合理设计共同导致:

  • 代码里错误绑定了进程级process.stdin全局流事件:每次调用newQuery都会给全局标准输入流注册data/end监听器,还执行process.stdin.resume()让流保持常驻,监听器永远不会被移除,不仅造成内存泄漏,还会干扰事件循环调度。
  • 数据库连接逻辑重复绑定事件、逻辑冲突:同时给connection绑定了connect事件监听器写SQL执行逻辑,又在connect()方法里传入单独的connected回调,两套连接成功逻辑同时触发,会导致连接状态混乱。
  • 超大结果集同步处理阻塞主线程:无过滤的SELECT *全表查询如果返回几十万甚至上百万行,row事件是逐行同步触发的,逐行遍历列、组装数组的JS逻辑全部跑在Node.js主线程,连续数分钟的同步数据处理会直接占满事件循环,所有其他请求都无法被调度。
  • 路由代码有逻辑bug:/loadAllEvents路由里,在发起异步查询的下一行直接写了res.status(401),请求刚进来就会提前发送401响应,等查询回调执行时res已经结束,会抛出响应发送错误。
  • 架构设计不合理:数分钟级别的长耗时查询本身就不应该和普通短API请求放在同一个Web服务进程/连接池里处理,长查询占满连接、占满内存的情况下,短请求自然拿不到执行资源。

分步修复方案

第一步:修复SQLServer.js的现有代码bug

删除冗余、错误的全局流绑定、重复连接逻辑,不要在数据库操作方法里触碰任何全局process对象,修正后的newQuery方法如下:

var Connection = require('tedious').Connection;
var Request = require('tedious').Request;

module.exports = {
  newQuery: function (query, config, callback) {
    const connection = new Connection(config);
    const resultValue = {
      columnTitle: [],
      line: []
    };

    // 统一处理连接事件
    connection.on('connect', function (err) {
      if (err) {
        console.log('数据库连接失败: ', err);
        callback(err, null);
        return;
      }

      const request = new Request(query, function (err, rowCount) {
        if (err) {
          console.log('SQL执行错误: ', err);
          callback(err, null);
          connection.close();
          return;
        }
      });

      // 解析列元数据
      request.on('columnMetadata', function (columns) {
        resultValue.columnTitle = columns.map(col => col.colName);
      });

      // 逐行解析数据,大结果集场景下不要在此处加复杂同步逻辑
      request.on('row', function (columns) {
        const row = columns.map(column => column.value === null ? 'NULL' : column.value);
        resultValue.line.push(row);
      });

      // 请求完成后关闭连接
      request.on('requestCompleted', function () {
        connection.close();
      });

      // 连接关闭时返回最终结果
      connection.on('end', function () {
        callback(null, resultValue);
      });

      connection.execSql(request);
    });

    // 发起连接,不要传入额外的连接回调,所有逻辑统一放在connect事件中处理
    connection.connect();
  }
};

回调采用Node.js通用的错误优先风格callback(err, result),方便上层统一捕获错误。

第二步:修复路由层代码bug

删除提前发送401的错误逻辑,统一在异步回调里处理响应:

// Server.js 对应路由修改
app.post('/loadAllEvents', function (req, res) {
  machineDataAnalysis.getAllEvents(function (err, resp) {
    if (err) {
      return res.status(500).send('查询失败');
    }
    res.status(200).send(JSON.stringify(resp));
  })
})

对应的getAllEvents方法需要适配错误优先回调,透传数据库层返回的错误即可。

第三步:解决大结果集阻塞主线程问题

如果查询返回结果超过10万行,逐行同步组装数组的逻辑依然会阻塞事件循环,可按以下方式优化:

  • 优化SQL逻辑:不要做无过滤的SELECT *全表查询,加分页、条件过滤,控制单次查询返回的行数在1万行以内,从根源上减少主线程同步处理的压力。
  • 开启tedious的流模式:不要把所有行存在内存数组中,直接把查询结果以流的方式转发给响应,逐行处理完就直接发给客户端,不需要在服务器内存里攒全量结果,既省内存又不会长时间阻塞主线程。
  • 配置独立的数据库连接池:不要每次查询都新建数据库连接,用连接池维护长连接,把长查询和短查询分到两个独立的连接池,长查询池配置较小的最大连接数(比如2-3个),短查询池配置较大的连接数,避免长查询把所有数据库连接占满导致短请求拿不到连接。

第四步:架构层面彻底解决长查询阻塞

数分钟级别的长查询不要直接在Express接口里同步等待返回,改成异步任务模式:

  1. 接口收到长查询请求后,直接返回202 Accepted,同时生成唯一任务ID存到缓存/数据库中,告知用户任务已提交。
  2. 把长查询任务丢给独立的工作进程处理(可以用worker_threads开单独线程,或者单独部署任务服务),工作进程处理完之后把结果存到缓存/对象存储里,更新任务状态为完成。
  3. 前端拿到任务ID之后,通过轮询或WebSocket请求任务状态接口,等任务完成后再拉取结果。
    这种模式下长查询完全在单独的进程/线程里运行,不会占用Web服务的主线程资源,普通短请求不会被阻塞。

额外注意点
  • 不要在Node.js主线程里写CPU密集型逻辑,比如大数组遍历、复杂数据计算、大对象JSON序列化,这类操作如果耗时超过100ms就会明显阻塞其他请求,要么拆分成分片处理,要么丢给worker线程执行。
  • 给长查询设置合理的超时时间,避免慢SQL占着连接数小时不释放。
  • 数据库层面给常用查询条件加索引,把几分钟的慢查询优化到秒级,是成本最低的优化方案。

内容的提问来源于stack exchange,提问作者Luke_Screwdriver

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 10:57:17