MySQL 是 Node.js 生态中最常见的关系型数据库后端之一。虽然也有 mysql 这个经典驱动,但目前更推荐使用 mysql2,因为它完全兼容 mysql 包的 API,同时带来更好的性能和更现代的 Promise 支持。作为直接与 MySQL 服务器通信的原生驱动,mysql2 避免了 ORM 额外的抽象层,在处理性能敏感或需要精细 SQL 控制的项目中非常实用。
1. 快速连接 MySQL
用 mysql2 连接 MySQL 需要先安装:
npm install mysql2
最基本的连接方式是在需要时创建连接:
const mysql = require('mysql2');
// 创建单次连接
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'your_password',
database: 'test_db'
});
connection.query('SELECT 1 + 1 AS result', (err, results) => {
if (err) throw err;
console.log(results[0].result); // 2
});
// 结束后释放连接
connection.end();
这种方式适合命令行脚本或短生命周期任务,但对于 Web 服务来说,每次都建立连接的开销太大。正确做法是使用连接池。
2. 连接池:提高并发与性能
mysql2 内置了连接池实现,通过 mysql.createPool 创建:
const mysql = require('mysql2');
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'your_password',
database: 'test_db',
waitForConnections: true, // 当无可用连接时排队等待
connectionLimit: 10, // 最大连接数
queueLimit: 0 // 排队最大请求数(0 表示无限制)
});
// 通过 pool 执行查询
pool.query('SELECT NOW()', (err, results) => {
if (err) throw err;
console.log(results);
});
// 也可以获取一个连接手动管理
pool.getConnection((err, conn) => {
if (err) throw err;
conn.query('SELECT * FROM users', (err, rows) => {
conn.release(); // 一定要释放回池中
if (err) throw err;
console.log(rows);
});
});
连接池会预先创建若干个连接并保持存活,当有查询请求时直接复用,省去频繁的 TCP 握手和认证开销。connectionLimit 需要根据数据库服务器性能和并发量来设置,太小则并发时排队等待,太大则加重数据库负担。一般建议从 10-20 开始压测调整。
现代项目通常推荐用 Promise 版本,mysql2 默认原生支持:
const mysql = require('mysql2/promise'); // 注意引入路径
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'your_password',
database: 'test_db',
connectionLimit: 10
});
async function getUsers() {
const [rows] = await pool.query('SELECT * FROM users WHERE age > ?', [18]);
return rows;
}
query() 返回一个二维数组 [rows, fields],符合 mysql2/promise 的约定。这种写法能充分利用 async/await,结构更清晰,错误处理也更直接。
3. 事务处理
事务是保证数据一致性的重要手段。mysql2 支持原生的手动事务控制,以及调用 pool.getConnection() 后使用 beginTransaction、commit 和 rollback。
回调风格的手动事务
pool.getConnection((err, conn) => {
if (err) throw err;
conn.beginTransaction(err => {
if (err) { conn.release(); throw err; }
conn.query('UPDATE accounts SET balance = balance - 100 WHERE id = ?', [1], (err) => {
if (err) {
return conn.rollback(() => {
conn.release();
throw err;
});
}
conn.query('UPDATE accounts SET balance = balance + 100 WHERE id = ?', [2], (err) => {
if (err) {
return conn.rollback(() => {
conn.release();
throw err;
});
}
conn.commit(err => {
if (err) {
return conn.rollback(() => {
conn.release();
throw err;
});
}
console.log('转账成功');
conn.release();
});
});
});
});
});
回调嵌套较深,容易形成“回调地狱”。
Promise + async/await 风格(推荐)
const mysql = require('mysql2/promise');
const pool = mysql.createPool({...});
async function transferMoney(fromId, toId, amount) {
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
const [fromAccount] = await conn.query(
'SELECT balance FROM accounts WHERE id = ? FOR UPDATE',
[fromId]
);
if (fromAccount[0].balance < amount) {
throw new Error('余额不足');
}
await conn.query(
'UPDATE accounts SET balance = balance - ? WHERE id = ?',
[amount, fromId]
);
await conn.query(
'UPDATE accounts SET balance = balance + ? WHERE id = ?',
[amount, toId]
);
await conn.commit();
console.log('转账成功');
} catch (err) {
await conn.rollback();
console.error('事务回滚,转账失败:', err.message);
throw err;
} finally {
conn.release(); // 无论如何都要释放连接
}
}
关键点:
- 必须获取一个专用连接:事务需要在同一个连接上执行,连接池的
pool.query会从池中任意取出一个连接,可能执行到一半释放给其他请求。所以事务操作必须调用pool.getConnection()获取一个独占连接,并在结束时release()。 FOR UPDATE行锁:在读取账户余额时添加FOR UPDATE可以锁定该行,防止并发转账导致数据错误。- 错误回滚:任何一步出错都要
rollback,否则数据库会处于不一致状态。 - finally 释放:
conn.release()必须在finally中调用,确保无论事务成功与否连接都归还连接池,避免池中连接被耗尽。
4. 预处理语句与 SQL 注入防护
mysql2 支持两种参数化查询方式,可以有效防止 SQL 注入。
占位符 ? 或 ??:
// ? 代表值,会自动转义
const [rows] = await pool.query(
'SELECT * FROM users WHERE name = ? AND age > ?',
['Alice', 18]
);
// ?? 代表标识符(表名、列名),会自动加反引号
const tableName = 'users';
const [rows2] = await pool.query(
'SELECT ?? FROM ??',
['id', tableName]
);
驱动内部会对传入的参数根据类型进行转义,数字直接转换,字符串会添加引号并转义特殊字符。开发者不要手动拼接 SQL 字符串,否则容易引入 SQL 注入漏洞。
真正的预处理语句(Prepared Statement) 在服务器端编译一次后多次执行,适合批量插入等场景:
const [rows] = await pool.execute(
'SELECT * FROM users WHERE id = ?',
[123]
);
execute() 方法会发送预处理请求到 MySQL 服务器,性能优于多次执行相同结构的 query(),而且自动进行参数转义。
5. 池化实践建议
在实际项目中,mysql2 的连接池配置和生命周期管理需要注意以下几点:
- 环境变量配置:将数据库凭证、连接数等放在环境变量中,方便不同环境切换。
- 连接超时与断连重试:可以在创建连接池时设置
connectTimeout、acquireTimeout等,并监听error事件处理连接丢失。 - 定期探活:可在应用层每隔一段时间执行简单的
SELECT 1来保持连接不被服务器端关闭,或启用keepAliveInitialDelay选项。 - 配合异步流程:在 Express/Koa 中,通常创建一次连接池并在应用启动时挂载到全局,不要在每次请求时新建池。
示例使用环境变量:
const pool = mysql.createPool({
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || 'app',
connectionLimit: parseInt(process.env.DB_POOL_SIZE, 10) || 10
});
6. 与 ORM 的关系
mysql2 提供了最底层的数据库驱动能力。很多项目中会进一步封装到 DAO 层,或者使用 ORM(如 Sequelize、TypeORM、Prisma)来提供更高层的抽象。但理解和掌握 mysql2 的原生用法依然重要:
- 精细性能调优时需要分析生成 SQL 语句。
- ORM 无法实现的复杂查询或批量操作,需要回退到原生查询。
- 理解连接池和事务在驱动层的实现机制,有助于排查 ORM 层的连接泄漏或死锁问题。
总之,mysql2 是 Node.js 生态中操作 MySQL 最直接、性能最好的原生驱动。通过合理使用连接池管理并发、谨慎处理事务保证一致性,并结合 Promise 和 async/await 编写清晰的异步代码,就能构建出稳定高效的数据库访问层。