在前一章中,我们使用以下函数创建和获取数据库连接:
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.h
和ConnectionPool.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(fullConnetionName); // [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.h 和 ConnectionPool.cpp。