MySQL 的数据 Node.js 驱动装起来只要一行命令,但真正踩过坑的数据人知道,这玩意儿的数据问题从来不在安装上。我见过太多项目在本地跑得好好的数据,一上生产环境就报
Too many connections,数据然后开发者一脸茫然地重启服务——这根本不是数据在解决问题,是数据在赌运气。Node.js 操作数据库的数据核心矛盾在于:它的异步模型和数据库连接的生命周期天然不匹配。一个 HTTP 请求进来,数据你查一次库,数据连接建立、数据查询、数据关闭,数据看起来没问题。数据但 QPS 一上来,数据连接创建和销毁的开销直接把 CPU 吃满。所以连接池不是可选项,是必选项。
const mysql = require('mysql2/promise'); const pool = mysql.createPool({ host: 'localhost', user: 'root', database: 'test', waitForConnections: true, connectionLimit: 10, queueLimit: 0 });这段代码几乎每篇教程都会贴。但
connectionLimit: 10到底意味着什么?意味着同时最多有 10 个连接在工作,第 11 个请求进来时,它会排队等。如果队列满了——默认queueLimit: 0表示无限排队——内存就会涨。我见过一个项目连接池设了 5,结果定时任务和 API 共用这个池子,定时任务跑批的时候 API 全部超时。排查了半天,最后发现是池子太小。用 mysql2 而不是 mysql,理由很直接:mysql2 支持 Promise,原生 mysql 的回调写法在 async/await 时代就是给自己找麻烦。而且 mysql2 的性能更好,对预处理语句的支持也更完整。这个选择没什么好犹豫的。
预处理语句值得单独说。拼接 SQL 字符串是安全事故的温床,但很多人不知道的是,即使用参数化查询,也有坑:
// 错误示范 const sql = `SELECT * FROM users WHERE id = ${ userId}`; // 正确做法 const [rows] = await pool.execute('SELECT * FROM users WHERE id = ?', [userId]);注意这里用的是
execute而不是query。区别在于execute会走预处理语句协议,query只是做了转义。对于需要重复执行的 SQL,execute在数据库端会缓存执行计划,性能更好。但有个细节:execute对某些数据类型(比如IN子句里的数组)支持不好,这时候还是得用query加手动转义。另一个真实场景:事务。Node.js 里写事务最怕的就是忘记释放连接。正确的写法是从池子里拿一个连接,用完必须
release:const conn = await pool.getConnection(); try { await conn.beginTransaction(); await conn.execute('UPDATE accounts SET balance = balance - ? WHERE id = ?', [100, 1]); await conn.execute('UPDATE accounts SET balance = balance + ? WHERE id = ?', [100, 2]); await conn.commit(); } catch (err) { await conn.rollback(); throw err; } finally { conn.release(); }finally块里的release是保命的。漏掉它,连接池很快就会被耗尽,然后整个服务挂掉。我就因为这个 bug 在凌晨两点被叫起来过。MongoDB 的情况不太一样。它不需要连接池配置得那么精细,因为驱动自己管得不错。但 Mongoose 的
bufferCommands默认开启,意味着数据库没连上时,查询会缓存起来而不是直接报错。这在开发时很方便,在生产环境却可能让你误以为一切正常,直到缓存堆积把内存撑爆。建议生产环境关掉它。选型上,关系型数据用 MySQL + mysql2,文档型用 MongoDB + 原生驱动就够了。ORM 不是必需品,尤其当你的 SQL 稍微复杂一点,ORM 生成的查询往往比你手写的慢一个数量级。我个人的习惯是:简单 CRUD 可以用 ORM 省事,复杂查询直接写 SQL。
顶: 4758踩: 1






评论专区