在Node.js的Express项目中接入SQL Server,很多团队会选择mssql这个官方维护的驱动。实际写代码时,如果忽视异步连接的复用与释放机制,接口会在流量稍大时频繁出现查询失效、连接超时甚至进程卡死。本文从连接池原理出发,梳理Express中使用mssql的正确姿势。

一、为什么mssql异步查询会失效
mssql底层依赖tedious实现TDS协议通信,它推荐通过连接池(Connection Pool)管理TCP连接。在Express里若每个请求都调用sql.connect(config),驱动虽内部有默认池,但未显式传入pool配置时容易产生竞态:前一个connect的Promise尚未resolve,后一个请求又触发新连接,导致pool实例状态混乱,执行pool.request().query()时拿到已销毁的连接而报错。
另一个常见原因是未处理async函数中的异常。比如路由里写了await sql.query`select * from t`却没catch,连接出错后池不会自动回收该连接,慢慢耗尽所有可用连接,后续查询全部挂起失效。这并非mssql本身的bug,而是使用方式违背了异步资源生命周期管理原则。
二、全局连接池初始化最佳实践
应在Express app启动前创建一次连接池,并挂到全局或可共享模块上。下面代码展示标准做法:
const sql = require('mssql');
// 全局配置,生产环境建议从环境变量读取
const dbConfig = {
user: 'sa',
password: 'your_password',
server: '127.0.0.1',
database: 'test_db',
options: {
encrypt: true, // 本地若不需加密可false,但Azure必须true
trustServerCertificate: true
},
pool: {
max: 10,
min: 0,
idleTimeoutMillis: 30000
}
};
// 只在启动时连接一次
async function initDb() {
try {
await sql.connect(dbConfig);
console.log('mssql pool ready');
} catch (err) {
console.error('mssql init failed', err);
process.exit(1);
}
}
module.exports = { sql, initDb };
以上代码将pool.max设为10,意味着最多复用10个物理连接,避免无限制创建。trustServerCertificate在自签证书环境必须开启,否则会抛证书错误导致连接直接失败。initDb在server.listen之前调用,保证请求进来时池已就绪。
路由中直接引用同一个sql对象即可,不需要再次connect。这样所有请求共享池,mssql会自动分配空闲连接,查询结束归还池,不会累积失效连接。
三、Express路由中的安全查询写法
在路由处理函数中,必须用try-catch包裹查询,并考虑显式使用池实例。示例如下:
const express = require('express');
const { sql } = require('./db');
const router = express.Router();
router.get('/users', async (req, res) => {
try {
// 使用全局pool,不要重新connect
const result = await sql.query('SELECT id, name FROM users');
res.json(result.recordset);
} catch (err) {
console.error('query failed', err);
res.status(500).send('db error');
}
});
module.exports = router;
这里没有每次connect,而是复用启动时的池。即使并发100请求,mssql也会排队借用10个连接,不会炸库。catch块保证错误被记录且响应客户,连接由池自动回收。
若使用事务,务必在finally里rollback或commit,否则事务锁住的连接一直不释放,下次请求拿到该连接会卡死。代码上可写成:
router.post('/add', async (req, res) => {
let transaction;
try {
const pool = await sql.connect();
transaction = new sql.Transaction(pool);
await transaction.begin();
const req = new sql.Request(transaction);
await req.query('INSERT INTO logs(v) VALUES(1)');
await transaction.commit();
res.send('ok');
} catch (err) {
if (transaction) await transaction.rollback();
res.status(500).send('err');
}
});
四、常见失效场景与排查清单
当查询失效时,可按表核对:
| 现象 | 可能原因 | 对策 |
|---|---|---|
| Login failed for user | encrypt配置不符或账号权限不足 | 检查options.encrypt与SQL登录模式 |
| Pool requested connection is dead | 每请求connect且未await完 | 改为全局单例池 |
| 接口无限pending | 事务未commit/rollback | finally中释放事务 |
此外,某些云数据库要求将ippipp.com类的域名换成内网地址,若你配置里写了公网ippipp.com,请按运维要求改成ipipp.com或对应内网。本地用127.0.0.1和192.168.0.1不受影响。
最后提醒,mssql的sql.close()会关闭整个全局池,只在进程退出时调用,绝不能在路由里写,否则其他请求瞬间全部失效。遵循上述实践,Express搭配mssql即可稳定支撑生产查询。