人人都会AI编程

MySQL:mysql2 原生驱动、连接池、事务处理

更新时间:2026-07-11

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() 后使用 beginTransactioncommitrollback

回调风格的手动事务

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 的连接池配置和生命周期管理需要注意以下几点:

  • 环境变量配置:将数据库凭证、连接数等放在环境变量中,方便不同环境切换。
  • 连接超时与断连重试:可以在创建连接池时设置 connectTimeoutacquireTimeout 等,并监听 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 编写清晰的异步代码,就能构建出稳定高效的数据库访问层。