Qt資料庫之資料庫連線池
在前面的章節裡,我們使用了下面的函式建立和取得資料庫連線:
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); }
雖然抽象出了連線的建立和獲取,但是有幾個弊端:
- 需要維護連線的名字
- 獲取連線的時候需要傳入連線的名字
- 獲取連線的時候不知道連線是否已經被使用,使用多執行緒的時候,每個執行緒都必須使用不同的連線
- 控制連線的最大數量比較困難,因為不能在程式裡無限制的建立連線
- 連線斷了後不會自動重連
- 刪除連線不方便
這一節我們將建立一個簡易的資料庫連線池,就是為了解決上面的幾個問題。使用資料庫連線池後,只需要關心下面 3 個函式,而且剛剛提到的那些弊端都通過連線池解決了,對呼叫者是透明的。
功能 | 程式碼 |
---|---|
獲取連線 | QSqlDatabase db = ConnectionPool::openConnection() |
釋放連線 | ConnectionPool::closeConnection(db) |
關閉連線池 | ConnectionPool::release() // 一般在 main() 函式返回前呼叫 |
資料庫連線池的使用
在具體介紹資料庫連線池的實現之前,先來看看怎麼使用。
#include "ConnectionPool.h" #include <QDebug> 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. 連線使用完後需要釋放回資料庫連線池 ConnectionPool::closeConnection(db); } int main(int argc, char *argv[]) { foo(); ConnectionPool::release(); // 4. 釋放資料庫連線 return 0; }
資料庫連線池的特點
- 獲取連線時不需要了解連線的名字
- 支援多執行緒,保證獲取到的連線一定是沒有被其他執行緒正在使用
- 按需建立連線
- 可以建立多個連線
- 可以控制連線的數量
- 連線被複用,不是每次都重新建立一個新的連線
- 連線斷開了後會自動重連
- 當無可用連線時,獲取連線的執行緒會等待一定時間嘗試繼續獲取,直到超時才會返回一個無效的連線
- 關閉連線很簡單
資料庫連線池的實現
資料庫連線池的實現只需要 2 個檔案:ConnectionPool.h
和 ConnectionPool.cpp
。下面會列出檔案的內容加以介紹。
ConnectionPool.h
#ifndef CONNECTIONPOOL_H
#define CONNECTIONPOOL_H
#include <QtSql>
#include <QQueue>
#include <QString>
#include <QMutex>
#include <QMutexLocker>
class ConnectionPool {
public:
static void release(); // 關閉所有的資料庫連線
static QSqlDatabase openConnection(); // 獲取資料庫連線
static void closeConnection(QSqlDatabase connection); // 釋放資料庫連接回連線池
~ConnectionPool();
private:
static ConnectionPool& getInstance();
ConnectionPool();
ConnectionPool(const ConnectionPool &other);
ConnectionPool& operator=(const ConnectionPool &other);
QSqlDatabase createConnection(const QString &connectionName); // 建立資料庫連線
QQueue<QString> usedConnectionNames; // 已使用的資料庫連線名
QQueue<QString> unusedConnectionNames; // 未使用的資料庫連線名
// 資料庫資訊
QString hostName;
QString databaseName;
QString username;
QString password;
QString databaseType;
bool testOnBorrow; // 取得連線的時候驗證連線是否有效
QString testOnBorrowSql; // 測試訪問資料庫的 SQL
int maxWaitTime; // 獲取連線最大等待時間
int waitInterval; // 嘗試獲取連線時等待間隔時間
int maxConnectionCount; // 最大連線數
static QMutex mutex;
static QWaitCondition waitConnection;
static ConnectionPool *instance;
};
#endif // CONNECTIONPOOL_H
openConnection()
用於從連線池裡獲取連線。closeConnection(QSqlDatabase connection)
並不會真正的關閉連線,而是把連線放回連線池複用。連線的底層是通過 Socket 來通訊的,建立 Socket 連線是非常耗時的,如果每個連線都在使用完後就給斷開 Socket 連線,需要的時候再重新建立 Socket連線是非常浪費的,所以要儘量的複用以提高效率。release()
真正的關閉所有的連線,一般在程式結束的時候才呼叫,在 main() 函式的 return 語句前。usedConnectionNames
儲存正在被使用的連線的名字,用於保證同一個連線不會同時被多個執行緒使用。unusedConnectionNames
儲存沒有被使用的連線的名字,它們對應的連線在呼叫openConnection()
時返回。- 如果
testOnBorrow
為 true,則連線斷開後會自動重新連線(例如資料庫程式崩潰了,網路的原因等導致連線斷開了)。但是每次獲取連線的時候都會先查詢一下資料庫,如果發現連線無效則重新建立連線。testOnBorrow
為 true 時,需要提供一條 SQL 語句用於測試查詢,例如 MySQL 下可以用SELECT 1
。如果testOnBorrow
為 false,則連線斷開後不會自動重新連線。需要注意的是,Qt 裡已經建立好的資料庫連線當連線斷開後呼叫 QSqlDatabase::isOpen() 返回的值仍然是 true,因為先前的時候已經建立好了連線,Qt 裡沒有提供判斷底層連線斷開的方法或者訊號,所以 QSqlDatabase::isOpen() 返回的仍然是先前的狀態 true。 testOnBorrowSql
為測試訪問資料庫的 SQL,一般是一個非常輕量級的 SQL,如SELECT 1
。- 獲取連線的時候,如果沒有可用連線,我們的策略並不是直接返回一個無效的連線,而是等待
waitInterval
毫秒,如果期間有連線被釋放回連線池裡就返回這個連線,沒有就繼續等待waitInterval
毫秒,再看看有沒有可用連線,直到等待maxWaitTime
毫秒仍然沒有可用連線才返回一個無效的連線。 - 因為我們不能在程式裡無限制的建立連線,用
maxConnectionCount
來控制建立連線的最大數量。
ConnectionPool.cpp
#include "ConnectionPool.h"
#include <QDebug>
QMutex ConnectionPool::mutex;
QWaitCondition ConnectionPool::waitConnection;
ConnectionPool* ConnectionPool::instance = NULL;
ConnectionPool::ConnectionPool() {
// 建立資料庫連線的這些資訊在實際開發的時都需要通過讀取配置檔案得到,
// 這裡為了演示方便所以寫死在了程式碼裡。
hostName = "127.0.0.1";
databaseName = "qt";
username = "root";
password = "root";
databaseType = "QMYSQL";
testOnBorrow = true;
testOnBorrowSql = "SELECT 1";
maxWaitTime = 1000;
waitInterval = 200;
maxConnectionCount = 5;
}
ConnectionPool::~ConnectionPool() {
// 銷燬連線池的時候刪除所有的連線
foreach(QString connectionName, usedConnectionNames) {
QSqlDatabase::removeDatabase(connectionName);
}
foreach(QString connectionName, unusedConnectionNames) {
QSqlDatabase::removeDatabase(connectionName);
}
}
ConnectionPool& ConnectionPool::getInstance() {
if (NULL == instance) {
QMutexLocker locker(&mutex);
if (NULL == instance) {
instance = new ConnectionPool();
}
}
return *instance;
}
void ConnectionPool::release() {
QMutexLocker locker(&mutex);
delete instance;
instance = NULL;
}
QSqlDatabase ConnectionPool::openConnection() {
ConnectionPool& pool = ConnectionPool::getInstance();
QString connectionName;
QMutexLocker locker(&mutex);
// 已建立連線數
int connectionCount = pool.unusedConnectionNames.size() + pool.usedConnectionNames.size();
// 如果連線已經用完,等待 waitInterval 毫秒看看是否有可用連線,最長等待 maxWaitTime 毫秒
for (int i = 0;
i < pool.maxWaitTime
&& pool.unusedConnectionNames.size() == 0 && connectionCount == pool.maxConnectionCount;
i += pool.waitInterval) {
waitConnection.wait(&mutex, pool.waitInterval);
// 重新計算已建立連線數
connectionCount = pool.unusedConnectionNames.size() + pool.usedConnectionNames.size();
}
if (pool.unusedConnectionNames.size() > 0) {
// 有已經回收的連線,複用它們
connectionName = pool.unusedConnectionNames.dequeue();
} else if (connectionCount < pool.maxConnectionCount) {
// 沒有已經回收的連線,但是沒有達到最大連線數,則建立新的連線
connectionName = QString("Connection-%1").arg(connectionCount + 1);
} else {
// 已經達到最大連線數
qDebug() << "Cannot create more connections.";
return QSqlDatabase();
}
// 建立連線
QSqlDatabase db = pool.createConnection(connectionName);
// 有效的連線才放入 usedConnectionNames
if (db.isOpen()) {
pool.usedConnectionNames.enqueue(connectionName);
}
return db;
}
void ConnectionPool::closeConnection(QSqlDatabase connection) {
ConnectionPool& pool = ConnectionPool::getInstance();
QString connectionName = connection.connectionName();
// 如果是我們建立的連線,從 used 裡刪除,放入 unused 裡
if (pool.usedConnectionNames.contains(connectionName)) {
QMutexLocker locker(&mutex);
pool.usedConnectionNames.removeOne(connectionName);
pool.unusedConnectionNames.enqueue(connectionName);
waitConnection.wakeOne();
}
}
QSqlDatabase ConnectionPool::createConnection(const QString &connectionName) {
// 連線已經建立過了,複用它,而不是重新建立
if (QSqlDatabase::contains(connectionName)) {
QSqlDatabase db1 = QSqlDatabase::database(connectionName);
if (testOnBorrow) {
// 返回連線前訪問資料庫,如果連線斷開,重新建立連線
qDebug() << "Test connection on borrow, execute:" << testOnBorrowSql << ", for" << connectionName;
QSqlQuery query(testOnBorrowSql, db1);
if (query.lastError().type() != QSqlError::NoError && !db1.open()) {
qDebug() << "Open datatabase error:" << db1.lastError().text();
return QSqlDatabase();
}
}
return db1;
}
// 建立一個新的連線
QSqlDatabase db = QSqlDatabase::addDatabase(databaseType, connectionName);
db.setHostName(hostName);
db.setDatabaseName(databaseName);
db.setUserName(username);
db.setPassword(password);
if (!db.open()) {
qDebug() << "Open datatabase error:" << db.lastError().text();
return QSqlDatabase();
}
return db;
}
為了支援多執行緒,使用了 QMutex,QWaitCondition 和 QMutexLocker 來保護共享資源 usedConnectionNames 和 unusedConnectionNames 的讀寫。
在建構函式裡初始化訪問資料庫的資訊和連線池的配置,為了方便所以都硬編碼寫在了程式碼裡,實際開發的時候這麼做是不可取的,都應該從配置檔案裡讀取,這樣當它們變化後只需要修改配置檔案就能生效,否則就需要修改程式碼,然後編譯,重新發布等。虛構函式裡真正的把所有連線和資料庫斷開。
ConnectionPool
使用了 Singleton 模式,保證在程式執行的時候只有一個物件被建立,getInstance()
用於取得這個唯一的物件。按理說使用 openConnection() 的方法在 Singleton 模式下的呼叫應該像這樣ConnectionPool::getInstance().openConnection(),但是我們實現的卻是 ConnectionPool::openConnection()
,因為我們把 openConnection()
也定義成靜態方法,在它裡面呼叫 getInstance()
訪問這個物件的資料,這樣做的好處即使用了 Singleton 的優勢,也簡化了 openConnection()
的呼叫。
呼叫 ConnectionPool::release()
會刪除 ConnectionPool
唯一的物件,在其虛構函式裡刪除所有的資料庫連線。
openConnection()
函式相對比較複雜,也是 ConnectionPool
的核心
- 如果沒有可複用連線
pool.unusedConnectionNames.size() == 0
且已經建立的連線數達到最大,則等待,等待期間有連線被釋放回連線池就複用這個連線,如果超時都沒有可用連線,則返回一個無效的連線QSqlDatabase()
。 - 如果沒有可複用連線,但是已經建立的連線數沒有達到最大,那麼就建立一個新的連線,並把這個連線的名字新增到
usedConnectionNames
。 - 如果有可複用的連線,則複用它,把它的名字從
unusedConnectionNames
裡刪除並且新增到usedConnectionNames
。
createConnection()
是真正建立連線的函式
- 如果連線已經被建立,不需要重新建立,而是複用它。
testOnBorrow
為 true 的話,返回這個連線前會先用 SQL 語句testOnBorrowSql
訪問一下資料庫,沒問題就返回這個連線,如果出錯則說明連線已經斷開了,需要重新和資料庫建立連線。 - 如果連線沒有被建立過,才會真的建立一個新的連線。
closeConnection()
並不是真的斷開連線
- 需要判斷連線是否我們建立的,如果不是就不處理。
- 把連線的名字從
usedConnectionNames
裡刪除並放到unusedConnectionNames
裡,表示這個連線已經被回收,可以被複用了。 - 喚醒一個等待的執行緒,告訴它有一個連線可用了。
測試
測試用例
:連線池允許最多建立 5 個連線,我們啟動 10 個執行緒用連線池裡獲取連線訪問資料庫。
ConnectionTestThread.h
#ifndef CONNECTIONTESTTHREAD_H
#define CONNECTIONTESTTHREAD_H
#include <QThread>
class ConnectionTestThread : public QThread {
protected:
void run();
};
#endif // CONNECTIONTESTTHREAD_H
ConnectionTestThread.cpp
#include "ConnectionTestThread.h"
#include "ConnectionPool.h"
void ConnectionTestThread::run() {
// 從資料庫連線池裡取得連線
QSqlDatabase db = ConnectionPool::openConnection();
qDebug() << "In thread run():" << db.connectionName();
QSqlQuery query(db);
query.exec("SELECT * FROM user where id=1");
while (query.next()) {
qDebug() << query.value("username").toString();
}
// 連線使用完後需要釋放回資料庫連線池
ConnectionPool::closeConnection(db);
}
main.cpp
#include "ConnectionTestThread.h"
#include "ConnectionPool.h"
#include <QApplication>
#include <QPushButton>
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
QPushButton *button = new QPushButton("Access Database");
button->show();
QObject::connect(button, &QPushButton::clicked, []() {
for (int i = 0; i < 10; ++i) {
ConnectionTestThread *thread = new ConnectionTestThread();
thread->start();
}
});
int ret = a.exec();
ConnectionPool::release(); // 程式結束時關閉連線,以免造成連線洩漏
return ret;
}
執行程式,點選按鈕 Access Database
,輸出如下:
In thread run(): Connection-1
Alice
In thread run(): Connection-2
Alice
In thread run(): Connection-3
Alice
In thread run(): Connection-4
Alice
In thread run(): Connection-5
Test connection on borrow, execute: SELECT 1 , for Connection-1
Alice
In thread run(): Connection-1
Test connection on borrow, execute: SELECT 1 , for Connection-2
Alice
In thread run(): Connection-2
Test connection on borrow, execute: SELECT 1 , for Connection-3
Alice
In thread run(): Connection-3
Test connection on borrow, execute: SELECT 1 , for Connection-4
Alice
In thread run(): Connection-4
Test connection on borrow, execute: SELECT 1 , for Connection-5
Alice
In thread run(): Connection-5
Alice
可以看到,前 5 個連線是新建立的,後面 5 個連線複用了已經建立的連線。
可以再做一下幾個測試,看看連線池是否都能正確的執行。
Case 1
- 點選按鈕
Access Database
,正常輸出。 - 然後關閉資料庫,點選按鈕
Access Database
,應該提示連不上資料庫。 - 啟動資料庫,點選按鈕
Access Database
,正常輸出。
Case 2
- 把執行緒數增加到 100 個,1000 個。
- 同時測試關閉和再次開啟資料庫。
Case 3
- 線上程的 run() 函式裡隨機等待一段時間,例如 0 到 100 毫秒。
資料庫連線池基本已經完成,但是並不是很完善。考慮一下如果我們設定最大連線數為 100,高峰期訪問比較多,建立滿了 100 個連線,但是當閒置下來後可能只需要 2 個連線,其餘 98 個連線都不長時間不用,但它們一直都和資料庫保持著連線,這對資源(Socket 連線)是很大的浪費。需要有這樣的機制,當發現連線一段時間沒有被使用後就把其關閉,並從 unusedConnectionNames
裡刪除。還有例如連線被分配後沒有釋放回連線池,即一直在 usedConnectionNames
裡面,即連線洩漏,超過一定時間後連線池應該主動把其回收。怎麼實現這些的功能,這裡就不在一一說明,大家獨自思考一下應該怎麼實現這些功能。