在前面的章节里,我们使用了下面的函数创建和取得数据库连接:
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"); db.setUserName("root"); db.setPassword("root");
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() { QSqlDatabase db = ConnectionPool::openConnection();
QSqlQuery query(db); query.exec("SELECT * FROM user where id=1");
while (query.next()) { qDebug() << query.value("username").toString(); }
}
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:
static QSqlDatabase openConnection(const QString &connectionName = QString());
private: static QSqlDatabase createConnection(const QString &connectionName); };
#endif
|
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) {
QString baseConnectionName = "conn_" + QString::number(quint64(QThread::currentThread()), 16); QString fullConnectionName = baseConnectionName + connectionName;
if (QSqlDatabase::contains(fullConnectionName)) { QSqlDatabase existingDb = QSqlDatabase::database(fullConnectionName);
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 { if (qApp != nullptr) { 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
| #ifndef THREAD_H #define THREAD_H
#include <QThread>
class Thread : public QThread { Q_OBJECT public: Thread();
protected: void run() override; };
#endif
|
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() { QSqlDatabase db = ConnectionPool::openConnection();
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(); 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。