Content Table

数据库连接池

在前面的章节里,我们使用了下面的函数创建和取得数据库连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void createConnectionByName(const QString &connectionName) {
QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL", connectionName);
db.setHostName("127.0.0.1");
db.setDatabaseName("qt"); // 如果是 SQLite 则为数据库文件路径
db.setUserName("root"); // 如果是 SQLite 不需要
db.setPassword("root"); // 如果是 SQLite 不需要

if (!db.open()) {
qDebug() << "Connect to MySql error: " << db.lastError().text();
return;
}
}

QSqlDatabase getConnectionByName(const QString &connectionName) {
return QSqlDatabase::database(connectionName);
}

虽然抽象出了连接的创建和获取,但是有几个弊端:

  • 需要我们维护连接的名字,不小心就重名了
  • 获取连接的时候需要传入连接的名字
  • 获取连接的时候不知道连接是否正在被使用,很容易一个线程中获取另外一个线程创建的数据库连接
  • 每次调用 createConnectionByName() 都会创建一个新的连接
  • 连接断开后不会自动重连
  • 需要手动释放连接

为了解决上面的几个问题,这一节我们将实现一个简易的数据库连接池。使用数据库连接池后,连接的创建、获取、释放自动释放等只需要使用下面 2 个函数,刚刚提到的那些弊端都通过连接池解决了。

功能 代码
获取连接 QSqlDatabase db = ConnectionPool::openConnection();
释放连接 使用连接的线程结束后自动释放连接

数据库连接池的使用

在具体介绍数据库连接池的实现之前,先来看看怎么使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <QDebug>
#include <QApplication>

#include "ConnectionPool.h"

void foo() {
// [1] 从数据库连接池里取得连接
QSqlDatabase db = ConnectionPool::openConnection();

// [2] 使用连接查询数据库
QSqlQuery query(db);
query.exec("SELECT * FROM user where id=1");

while (query.next()) {
qDebug() << query.value("username").toString();
}

// [3] 不用管连接的释放
}

int main(int argc, char *argv[]) {
QApplication app(argc, argv);
foo();
return app.exec();
}

就像上面程序所示,使用数据库连接池时不需要关系连接的创建、关闭等,只管用。

数据库连接池的特点

  • 获取连接时不需要了解连接的名字,连接池内部维护连接的名字
  • 支持多线程,保证获取到的连接一定是没有被其他线程正在使用
  • 按需创建连接
  • 可以创建多个连接
  • 可以控制连接的数量
  • 连接被复用,不是每次都重新创建一个新的连接(连接的创建是一个很消耗资源的过程)
  • 连接断开了后会自动重连
  • 当无可用连接时,获取连接的线程会等待一定时间尝试继续获取,直到取到有效连接或者超时返回一个无效的连接
  • 关闭连接很简单

数据库连接池的实现

数据库连接池的实现只需要 2 个文件:ConnectionPool.hConnectionPool.cpp,下面列出程序的内容加以介绍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#ifndef CONNECTIONPOOL_H
#define CONNECTIONPOOL_H

#include <QString>
#include <QSqlDatabase>
#include <QSqlQuery>

class ConnectionPool {
public:
/**
* @brief 获取数据库连接,连接使用完后不需要手动关闭,数据库连接池会在使用此连接的线程结束后自动关闭连接。
* 传入的连接名 connectionName 默认为空 (内部会为连接名基于线程的信息创建一个唯一的前缀),
* 如果同一个线程需要使用多个不同的数据库连接,可以传入不同的 connectionName
*
* @param connectionName 连接的名字
* @return 返回数据库连接
*/
static QSqlDatabase openConnection(const QString &connectionName = QString());

private:
static QSqlDatabase createConnection(const QString &connectionName); // 创建数据库连接
};

#endif // CONNECTIONPOOL_H
  • openConnection() 用于从连接池里获取连接
  • createConnection() 连接池内部用来创建连接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include "ConnectionPool.h"

#include <QDebug>
#include <QtSql>
#include <QString>
#include <QThread>
#include <QCoreApplication>

// 获取数据库连接
QSqlDatabase ConnectionPool::openConnection(const QString &connectionName) {
// 1. 创建连接的全名: 基于线程的地址和传入进来的 connectionName,因为同一个线程可能申请创建多个数据库连接
// 2. 如果连接已经存在,复用它,而不是重新创建
// 2.1 返回连接前访问数据库,如果连接断开,可以重新建立连接 (测试: 关闭数据库几分钟后再启动,再次访问数据库)
// 3. 如果连接不存在,则创建连接
// 4. 线程结束时,释放在此线程中创建的数据库连接

// [1] 创建连接的全名: 基于线程的地址和传入进来的 connectionName,因为同一个线程可能申请创建多个数据库连接
QString baseConnectionName = "conn_" + QString::number(quint64(QThread::currentThread()), 16);
QString fullConnectionName = baseConnectionName + connectionName;

if (QSqlDatabase::contains(fullConnectionName)) {
// [2] 如果连接已经存在,复用它,而不是重新创建
QSqlDatabase existingDb = QSqlDatabase::database(fullConnectionName);

// [2.1] 返回连接前访问数据库,如果连接断开,可以重新建立连接 (测试: 关闭数据库几分钟后再启动,再次访问数据库)
QSqlQuery query("SELECT 1", existingDb);

if (query.lastError().type() != QSqlError::NoError && !existingDb.open()) {
qDebug().noquote() << "Open datatabase error:" << existingDb.lastError().text();
return QSqlDatabase();
}

return existingDb;
} else {
// [3] 如果连接不存在,则创建连接
if (qApp != nullptr) {
// [4] 线程结束时,释放在此线程中创建的数据库连接
QObject::connect(QThread::currentThread(), &QThread::finished, qApp, [fullConnectionName] {
if (QSqlDatabase::contains(fullConnectionName)) {
QSqlDatabase::removeDatabase(fullConnectionName);
qDebug().noquote() << QString("Connection deleted: %1").arg(fullConnectionName);
}
});
}

return createConnection(fullConnectionName);
}
}

// 创建数据库连接
QSqlDatabase ConnectionPool::createConnection(const QString &connectionName) {
static int sn = 0;

// 创建一个新的数据库连接
QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL", connectionName);
db.setHostName("localhost");
db.setDatabaseName("qt");
db.setUserName("root");
db.setPassword("root");

if (db.open()) {
qDebug().noquote() << QString("Connection created: %1, sn: %2").arg(connectionName).arg(++sn);
return db;
} else {
qDebug().noquote() << "Create connection error:" << db.lastError().text();
return QSqlDatabase();
}
}
  • 基于线程构建连接的名字,这样就能保证不同的线程中连接的名字不会重复

    由于安全的原因,大概是 Qt 5.4 以后一个线程创建的连接不允许在其他线程中使用 (早一些的版本可以)。一个线程内的函数执行总是串行的,绝大多数时候一个线程内使用一个数据库连接就可以了,特殊情况下需要同时维护多个连接各自独立的状态时传入不同的连接名可以获取到不同的数据库连接。

  • 获取连接时,先判断此线程中是否有可用连接,如果有则重用,没有则创建

  • 连接不需要归还给连接池,因为连接与线程相关,同一个线程里代码是串行执行的

  • 连接不需要手动关闭,程序结束时会自动关闭

多线程测试

由于 Qt 5.12 中不允许数据库连接跨线程使用,测试一下多线程的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 文件名: Thread.h
#ifndef THREAD_H
#define THREAD_H

#include <QThread>

class Thread : public QThread {
Q_OBJECT
public:
Thread();

protected:
void run() override;
};

#endif // THREAD_H
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include "Thread.h"
#include "ConnectionPool.h"
#include <QDebug>

Thread::Thread() {

}

static void foo() {
// [1] 从数据库连接池里取得连接
QSqlDatabase db = ConnectionPool::openConnection();

// [2] 使用连接查询数据库
QSqlQuery query(db);
query.exec("SELECT * FROM user where id=1");

while (query.next()) {
qDebug() << query.value("username").toString();
}
}

void Thread::run() {
foo();
QThread::sleep(1);
foo();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <QDebug>
#include <QApplication>

#include "Thread.h"

int main(int argc, char *argv[]) {
QApplication app(argc, argv);

for (int i = 0; i < 10; ++i) {
Thread *t = new Thread();
t->start();

// 如果瞬间启动多个线程建立 MySQL 数据库连接,很可能会报异常 unable to allocate a MYSQL object
// 导致部分连接建立失败,于是等待 100 毫秒才启动下一个线程
QThread::msleep(100);
}

return app.exec();
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Connection created: conn_7fe983d5f790, sn: 1
"Alice"
Connection created: conn_7fe983d47590, sn: 2
Connection created: conn_7fe983d517b0, sn: 3
"Alice"
Connection created: conn_7fe983d40c60, sn: 4
"Alice"
"Alice"
Connection created: conn_7fe983d25710, sn: 5
Connection created: conn_7fe983d5b8e0, sn: 6
Connection created: conn_7fe983d3be90, sn: 7
"Alice"
"Alice"
"Alice"
Connection created: conn_7fe983d25200, sn: 8
Connection created: conn_7fe983d3d590, sn: 9
"Alice"
"Alice"
Connection created: conn_7fe983d41df0, sn: 10
"Alice"
"Alice"
Connection deleted: conn_7fe983d5f790
"Alice"
Connection deleted: conn_7fe983d47590
"Alice"
Connection deleted: conn_7fe983d517b0
"Alice"
Connection deleted: conn_7fe983d40c60
"Alice"
"Alice"
"Alice"
"Alice"
"Alice"
Connection deleted: conn_7fe983d25710
Connection deleted: conn_7fe983d5b8e0
Connection deleted: conn_7fe983d3d590
Connection deleted: conn_7fe983d25200
Connection deleted: conn_7fe983d3be90
"Alice"
Connection deleted: conn_7fe983d41df0

可以看到每个线程都创建了不同的连接,同一个线程里的连接进行了复用,线程结束后连接都自动释放掉了。

思考

一个简单数据库连接池的功能基本已经完成,但还有很多地方不完善,例如没有考虑限制连接的最大数量,而 MySQL 等数据库有连接数量的限制,没有对连接数进行控制,是因为我们觉得 Qt 程序一般不需要去控制连接数,有以下理由:

  • Qt 程序一般都是客户端的桌面程序,同一个程序中不太可能同时创建很多数据库连接,例如 100 个,如果真有,那么就可以考虑下设计是否合理
  • Qt 很少用来开发服务器端程序,访问数据库向前端提供服务,这时优先可以考虑使用 Java 等服务器端更成熟的方案
  • Qt 程序可能访问本地的 Sqlite 数据库的情况更多一些,作为客户端时直接访问远程的 MySQL 等安全上是不允许的,数据库一般都不允许外网访问

当然需要考虑高并发时,就需要实现更复杂的连接池,控制连接数、自动释放长时间不活跃的连接,需要使用计时器扫描连接状态,连接按照线程分组管理,归还连接到连接池等,早期 Qt 中允许数据库连接跨线程使用,实现过一个连接池,可以作为参考学习一下 ConnectionPool.hConnectionPool.cpp