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