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() 函數里隨機等待一段時間,例如 0100 毫秒。
數據庫連接池基本已經完成,但是並不是很完善。
考慮一下如果我們設置最大連接數為
100,高峰期訪問比較多,創建滿了 100 個連接,
但是當閑置下來后可能只需要 2 個連接,其余 98 個連接都不長時間不用,
但它們一直都和數據庫保持着連接,這對資源(Socket 連接)是很大的浪費。
需要有這樣的機制,當發現連接一段時間沒有被使用后就把其關閉,並從 unusedConnectionNames 里刪除。
還有例如連接被分配后沒有釋放回連接池,即一直在 usedConnectionNames 里面,即連接泄漏,
超過一定時間后連接池應該主動把其回收。怎么實現這些的功能,這里就不在一一說明,大家獨自思考一下應該怎么實現這些功能。

 

 

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 里面,即連接泄漏,超過一定時間后連接池應該主動把其回收。怎么實現這些的功能,這里就不在一一說明,大家獨自思考一下應該怎么實現這些功能。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM