Node.js与MySQL的组合是目前后端开发中非常常见的搭配,前者负责处理高并发的I/O请求,后者作为成熟的关系型数据库承担数据持久化的职责。要让这两者顺畅协作,关键在于选对驱动、理解连接的生命周期,并掌握连接池、预处理语句这些进阶手段。本文从环境准备讲起,逐步覆盖基础连接、CRUD操作、连接池优化、事务与错误处理,最后汇总开发中容易踩坑的地方,帮你建立起一套完整可落地的知识体系。

环境准备与驱动选择:mysql和mysql2该怎么选
在Node.js中连接MySQL,第一步是安装驱动。目前主流的驱动有两个:mysql和mysql2。mysql是最早出现的经典驱动,文档丰富、社区资料多,但它已经多年没有积极维护了。mysql2是在其基础上重写的现代化驱动,API几乎完全兼容mysql,同时支持Promise语法、预处理语句性能更好,并且是Sequelize、TypeORM、Prisma等主流ORM的底层依赖。对于新项目,强烈建议直接使用mysql2。
安装方式很简单,在项目根目录执行npm安装命令即可:
# 初始化项目并安装mysql2 npm init -y npm install mysql2
安装完成后,就可以创建一个db.js文件,把数据库连接逻辑封装起来。连接MySQL需要的基本参数包括主机地址、端口、用户名、密码和目标数据库名。建议把这些敏感信息放到环境变量中,而不是硬编码在代码里,避免提交到代码仓库造成泄露:
// db.js
const mysql = require('mysql2/promise');
const pool = mysql.createPool({
host: process.env.DB_HOST || '127.0.0.1',
port: 3306,
user: process.env.DB_USER || 'root',
password: process.env.DB_PASS || 'your_password',
database: process.env.DB_NAME || 'demo',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
module.exports = pool;
这里有两个容易混淆的概念需要厘清:createConnection创建的是单个连接,每次查询复用同一条物理连接;createPool创建的是连接池,内部维护多条连接,请求到来时自动分配空闲连接,用完自动归还。生产环境几乎都应该使用连接池,原因在下一节详细展开。
基础CRUD操作:查询、插入、更新与删除的正确写法
有了连接池之后,执行SQL就变得非常直接。mysql2的promise接口提供了query和execute两个方法,前者直接发送SQL文本,后者使用真正的服务端预处理语句。execute配合占位符使用可以有效防止SQL注入,是处理用户输入时的首选方式。
下面是一组典型的增删改查示例,涵盖了最常用的操作场景:
const pool = require('./db');
// 查询:使用execute配合占位符防止SQL注入
async function getUsers(minAge) {
const [rows] = await pool.execute(
'SELECT id, name, age FROM users WHERE age > ? ORDER BY id DESC LIMIT 20',
[minAge]
);
return rows;
}
// 插入:返回的结果中包含insertId和affectedRows
async function createUser(name, age) {
const [result] = await pool.execute(
'INSERT INTO users (name, age) VALUES (?, ?)',
[name, age]
);
return result.insertId;
}
// 更新
async function updateUserAge(id, age) {
const [result] = await pool.execute(
'UPDATE users SET age = ? WHERE id = ?',
[age, id]
);
return result.affectedRows > 0;
}
// 删除
async function deleteUser(id) {
const [result] = await pool.execute(
'DELETE FROM users WHERE id = ?',
[id]
);
return result.affectedRows;
}
有几个细节值得注意。第一,execute的返回值是一个数组,解构时第一个元素是结果集,第二个元素是字段元信息,很多人初学时忘了解构,把整个数组当成数据去遍历,结果拿到一堆奇怪的字段对象。第二,占位符只能替代值,不能替代表名、列名或SQL关键字,如果需要动态拼接表名,必须使用白名单校验,绝不能直接拼接用户输入。第三,批量插入时可以借助query配合展开运算符构造多条values,比循环单条插入效率高得多。
连接池的原理与参数调优
建立一条TCP连接到MySQL服务器,需要经历握手、认证等多个往返过程,开销不小。如果每个请求都新建连接,高并发下数据库的连接创建和销毁本身就会成为瓶颈,甚至把MySQL的max_connections上限打满。连接池的做法是预先建立若干条连接放入池中复用,请求到达时借出一条空闲连接,使用完毕后归还,从而把连接建立的成本摊薄到整个服务生命周期中。
连接池有几个核心参数直接影响性能和稳定性,需要根据实际业务仔细调整:
| 参数 | 含义 | 建议 |
|---|---|---|
| connectionLimit | 池中最大连接数 | 根据数据库实例规格和并发量设置,一般10到30之间 |
| waitForConnections | 无空闲连接时是否排队等待 | 生产环境设为true,避免直接抛错 |
| queueLimit | 等待队列的最大长度 | 设为0表示不限制,防止打爆内存可设上限 |
| connectTimeout | 建立连接的超时时间 | 默认10秒,跨机房场景可适当加大 |
| enableKeepAlive | 保持连接活跃 | 长空闲容易被服务端断开,建议开启 |
关于connectionLimit的取值,有一个常见的误区是越大越好。实际上MySQL每个连接都要消耗服务端内存和线程资源,连接数过高反而会引发上下文切换开销,吞吐量不升反降。一个粗略的参考公式是:连接池大小约等于CPU核心数的2到4倍,再结合压测数据微调。另外,如果部署了多个Node.js实例,每个实例都有独立的连接池,总连接数是实例数乘以池大小,必须确保不超过MySQL服务端的连接上限。
池中的连接也可能因为长时间空闲被MySQL的wait_timeout主动断开,之后再用就会报PROTOCOL_CONNECTION_LOST错误。除了开启enableKeepAlive,mysql2的连接池本身会在取出连接时做健康检测,坏连接会被移除并重建,所以使用池模式配合promise接口,绝大多数情况下不需要手写重连逻辑。
事务处理与错误监控:保证数据一致性
涉及多步写入的业务,比如转账操作中扣减A账户余额和增加B账户余额,必须放在同一个事务里,任何一步失败都要整体回滚。mysql2的promise接口通过getConnection从池中取出专用连接来执行事务,注意事务内的所有语句必须使用同一条连接,这也是不能直接用池的execute方法做事务的原因。
async function transfer(fromId, toId, amount) {
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
await conn.execute(
'UPDATE accounts SET balance = balance - ? WHERE id = ?',
[amount, fromId]
);
await conn.execute(
'UPDATE accounts SET balance = balance + ? WHERE id = ?',
[amount, toId]
);
await conn.commit();
} catch (err) {
await conn.rollback();
throw err;
} finally {
// 事务结束后务必归还连接,否则池会被耗尽
conn.release();
}
}
事务代码中最典型的错误是忘记在finally里调用release归还连接。一旦某次事务抛出异常且没有归还,这条连接就永远漏掉了,随着时间推移池中可用连接越来越少,最终所有请求都卡在等待连接上,表现为服务整体无响应却没有任何报错,排查起来非常困难。养成把release写在finally块里的习惯,可以从根本上规避这类连接泄漏问题。
除了事务,错误监控也不可忽视。建议在池上监听error事件,记录日志并接入告警;对关键的写操作,可以配置重试策略,对网络抖动导致的临时性失败做有限次数的重试。同时要注意,重试只适合幂等操作,非幂等的插入如果不确定是否已成功,盲目重试可能造成重复数据,此时应通过唯一索引约束配合冲突处理来解决。
常见问题与避坑清单
最后把实际项目中高频出现的几类问题汇总如下,遇到时可以按图索骥快速定位:
- 时间差8小时:Node.js进程与MySQL时区不一致导致。可以在连接配置里设置
timezone: '+08:00',或者统一让数据库和服务器使用UTC,在展示层做时区转换。 - ER_ACCESS_DENIED_ERROR:账号密码错误,或该账号没有从当前主机远程连接的权限。MySQL的账号体系是用户名加主机两部分构成的,用
user@'%'或指定IP授权后需刷新权限。 - 连接数耗尽:排查是否存在事务未释放连接、代码中重复创建池对象等泄漏点。全局只应存在一个池实例,建议做成单例模块导出。
- 大结果集内存溢出:一次性查询几十万行会把数据全部载入内存。应加上分页limit,或使用mysql2提供的stream模式逐行消费结果。
- SQL注入风险:任何拼接用户输入的SQL都是隐患,坚持使用execute加占位符,动态表名列名走白名单校验。
总体来说,Node.js连接MySQL本身并不复杂,真正的功夫在于细节:选好驱动、合理配置连接池、正确使用预处理语句和事务、及时释放连接、做好错误监控。把这些习惯固化下来,数据库层就能长期稳定地支撑业务增长。建议读者把文中的示例代码跑通一遍,再结合自己的业务场景做压测调优,实践中积累的经验往往比文档更有价值。