从事 Node 开发 web 服务已有一段时间, 数据库用的是 mysql, 每次执行数据库操作流程如下:创建一个连接, 执行 sql 语句, 然后关闭连接。 当用户量不大,并发执行数据库操作不多的时候, 运转一切正常。 当写一个爬虫功能时候,同时执行2000条数据入库操作时, 系统报错,提示超时了。 意识到问题的重要性后,努力学习了一番,使用连接池解决了这一并发执行数据库操作问题。在此, 分享并记录 node 中使用数据库连接池, 测试连接池优势等。
npm install mysql
所谓单次连接, 指的是最基本的连接方式, 每次执行数据库操作,都会打开一个连接, 执行完之后关闭连接。
我们把它封装成一个 返回 promise 对象的函数, 便于使用。
const mysql = require('mysql');
// 连接配置信息
const dbConfig = {
host : 'localhost',
user : 'me',
password : 'secret',
database : 'my_db'
};
// 导出对象
const imp = {
// 执行
do: (sql, para) => {
return new Promise((resolve, reject) => {
let conn = mysql.createConnection(dbConfig);
conn .connect();
conn.query(sql, para, (err, rows) => {
if (err) return reject(err);
return resolve(rows);
});
connection.end();
});
}
};
module.exports = imp;
其中连接配置还可以设置连接超时时间 connectTimeout,是否启用大数字 supportBigNumbers, 是否启用debug模式等,都是一些特殊场景需求,详情移步官网文档: https://www.npmjs.com/package/mysql
连接池的定义: 连接池是创建和管理一个连接的缓冲池的技术,这些连接准备好被任何需要它们的线程使用。
为每个用户打开和维护数据库连接,特别是对动态数据库驱动的网站应用程序的请求,代价高昂,浪费资源。在连接池中,在创建连接之后,将它放置在池中,并再次使用它,这样就不必建立新的连接。如果正在使用所有连接,则将创建一个新连接并将其添加到池中。连接池还减少了用户必须等待建立到数据库连接的时间。
同样的, 我们把它封装成一个 返回 promise 对象的函数, 便于使用。
const mysql = require('mysql');
// 连接池配置
const dbPoolConfig = {
host : 'localhost',
user : 'me',
password : 'secret',
database : 'my_db',
acquireTimeout: 15000, // 连接超时时间
connectionLimit: 100, // 最大连接数
waitForConnections: true, // 超过最大连接时排队
queueLimit: 0, // 排队最大数量(0 代表不做限制)
};
// 创建连接池
const pool = mysql.createPool(dbPoolConfig);
const imp = {
// 执行
query: (sql, para) => {
return new Promise((resolve, reject) => {
pool.getConnection((err, conn) => {
if (err) return reject(err);
conn.query(sql, para, (err, rows) => {
conn.release();
if (err) return reject(err);
return resolve(rows);
});
});
});
}
};
module.exports = imp;
连接池配置参数详解
(1)acquireTimeout: 表示连接超时时间, 默认是10000 ms; 最大连接数越大, 连接时间越长。建议设置 15000ms
(2)connectionLimit: 最大连接数, 默认是 10; 并发操作较大时,连接数越大, 执行速度较快。
经测试, 20000 并发执行数据库查询操作, 最大连接数为 10时, 响应时长 14508ms; 最大连接数设置50, 300等,响应时长接近 11000ms。所以,建议最大连接数设置100。
(3)waitforConnections: 超过最大连接数是否等待。 默认是等待, 若设置成false, 则超过最大连接数就报错。建议设置为true。
(4)queueLimit: 排队最大数量。 默认为无限制, 0 代表无限制。 建议设置为 0。
现在, 我们以一个查询请求作为测试用例, 查询请求中增加参数count, 代表并发执行数据库操作次数, 代码如下:
let start_time = new Date().getTime(); // 开始时间
let pAll = []; // promise 对象数组
let count = Number(ctx.query.count); // 执行次数
for (let i = 0; i < count; i++) {
pAll.push(doSql()); // 添加
}
// 开始执行
let result = await Promise.all(pAll);
let end_tiem = new Date().getTime(); // 结束时间
console.log(`response time: ${end_time - start_time}`);
ctx.response.body = { code: 0, total: result.length, data: result };
相差: 1.7 ms
相差: 3.9 ms
相差: 71 ms
相差: 8775 ms
我们发现并发执行1000次, 单次连接方式已经不能胜任了, 而连接池方式表现依然卓越。
并发执行10000次
| 连接方式 | 平均响应时长(ms) | 是否报错 | 性能 |
| ------- | ------- | ------| ------- |
| 单次连接 | 失败 | 是 | 不能使用 |
| 连接池 | 7567 | 否 | 较慢 |
连接池这种方式可以响应到1w的并发执行数,而且响应时间与执行次数几乎成倍数增加。
并发执行20000次
| 连接方式 | 平均响应时长(ms) | 是否报错 | 性能 |
| ------- | ------- | ------| ------- |
| 单次连接 | 失败 | 是 | 不能使用 |
| 连接池 | 10775 | 否 | 慢 |
当并发执行2w时候,连接池依然可以使用,且响应时间没有达到预期的两倍。
接着又测试了 4w 并发执行, 响应时间约 23.2s;
6w 并发执行, 响应时间约 39.2s;
8w 并发执行, 响应时间约 52.0s;
测试得出初步结论:
有时,我们会遇到数据库连接,执行一些错误。以下是一些常用命令,帮助我们了解当前数据库的配置。
查询最大连接数
show variables like '%max_connections%';
设置最大连接数
set global max_connections=1000;
响应的最大连接数
show global status like 'Max_used_connections';
查看线程信息
show status like 'Threads%'
睡眠连接超时数
show global variables like 'wait_timeout';
杀死连接id (表: INFORMATION_SCHEMA.PROCESSLIST)
kill 21120003
更多资料:
https://blog.csdn.net/caodongfang126/article/details/52764213/
https://www.cnblogs.com/wajika/p/6763181.html
https://blog.csdn.net/wzb56_earl/article/details/51868584
问题: 同事在测试时, 发现单次连接和连接池方式响应时间差不多。
解答: 经过仔细对比发现原因。
(1)同事使用的是本地服务,连接本地数据库,且数据库最大连接数配置为2001;所以在并发执行数据库数在2000以内, 单次连接和连接池方式效果几乎一样。
(2)不同的是,上文测试数据的环境是本地服务, 连接远程数据库,所以每个执行数据库操作,打开和关闭连接的代价就更高,即使在最大连接数之内,单次连接效果也比连接池速度慢。
(3)当并发执行数超过最大连接数时候, 单次连接就会报错, too many connections
; 而此时, 连接池依然可以使用,且执行速度也比较快。原因是使用连接池方式,我们设置了最大连接数是200(小于数据库的最大连接数), 超过200后,连接池中会把执行动作排队,按批次执行。
问题:通过mysql中执行show full processlist
,发现大量睡眠连接,是什么引起的,是否需要关闭等。
解答:
(1)引起大量sleep状态连接的原因是,我们创建连接池,其中的连接执行完后,释放之后,不会销毁,变成sleep状态,供下次调用。
(2)其实是不需要主动关闭的,因为睡眠连接池不会一直增加,最大数量取决于系统的最高执行数据库操作的并发量。
(3)数据库中执行 show global variables like '%timeout'
, 其中wait_timeout
就是sleep连接关闭时间,默认为8小时。 若想调整 set global wait_timeout=60
, 意味着1分钟后,所有sleep状态的连接都会关闭。