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接口里同步等待返回,改成异步任务模式:
- 接口收到长查询请求后,直接返回202 Accepted,同时生成唯一任务ID存到缓存/数据库中,告知用户任务已提交。
- 把长查询任务丢给独立的工作进程处理(可以用
worker_threads开单独线程,或者单独部署任务服务),工作进程处理完之后把结果存到缓存/对象存储里,更新任务状态为完成。 - 前端拿到任务ID之后,通过轮询或WebSocket请求任务状态接口,等任务完成后再拉取结果。
这种模式下长查询完全在单独的进程/线程里运行,不会占用Web服务的主线程资源,普通短请求不会被阻塞。
额外注意点
- 不要在Node.js主线程里写CPU密集型逻辑,比如大数组遍历、复杂数据计算、大对象JSON序列化,这类操作如果耗时超过100ms就会明显阻塞其他请求,要么拆分成分片处理,要么丢给worker线程执行。
- 给长查询设置合理的超时时间,避免慢SQL占着连接数小时不释放。
- 数据库层面给常用查询条件加索引,把几分钟的慢查询优化到秒级,是成本最低的优化方案。
内容的提问来源于stack exchange,提问作者Luke_Screwdriver
相关产品推荐
相关产品推荐

