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 的核心
  1. 如果没有可复用连接 pool.unusedConnectionNames.size() == 0 且已经创建的连接数达到最大,则等待,等待期间有连接被释放回连接池就复用这个连接,如果超时都没有可用连接,则返回一个无效的连接 QSqlDatabase()
  2. 如果没有可复用连接,但是已经创建的连接数没有达到最大,那么就创建一个新的连接,并把这个连接的名字添加到 usedConnectionNames
  3. 如果有可复用的连接,则复用它,把它的名字从 unusedConnectionNames 里删除并且添加到 usedConnectionNames
createConnection() 是真正创建连接的函数
  1. 如果连接已经被创建,不需要重新创建,而是复用它。testOnBorrow 为 true 的话,返回这个连接前会先用 SQL 语句 testOnBorrowSql 访问一下数据库,没问题就返回这个连接,如果出错则说明连接已经断开了,需要重新和数据库建立连接。
  2. 如果连接没有被创建过,才会真的建立一个新的连接。
closeConnection() 并不是真的断开连接
  1. 需要判断连接是否我们创建的,如果不是就不处理。
  2. 把连接的名字从 usedConnectionNames 里删除并放到 unusedConnectionNames 里,表示这个连接已经被回收,可以被复用了。
  3. 唤醒一个等待的线程,告诉它有一个连接可用了。

测试

测试用例:连接池允许最多创建 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
  1. 点击按钮 Access Database,正常输出。
  2. 然后关闭数据库,点击按钮 Access Database,应该提示连不上数据库。
  3. 启动数据库,点击按钮 Access Database,正常输出。
Case 2
  • 把线程数增加到 100 个,1000 个。
  • 同时测试关闭和再次打开数据库。
Case 3
  • 在线程的 run() 函数里随机等待一段时间,例如 0 到 100 毫秒。

数据库连接池基本已经完成,但是并不是很完善。考虑一下如果我们设置最大连接数为 100,高峰期访问比较多,创建满了 100 个连接,但是当闲置下来后可能只需要 2 个连接,其余 98 个连接都不长时间不用,但它们一直都和数据库保持着连接,这对资源(Socket 连接)是很大的浪费。需要有这样的机制,当发现连接一段时间没有被使用后就把其关闭,并从 unusedConnectionNames 里删除。还有例如连接被分配后没有释放回连接池,即一直在 usedConnectionNames 里面,即连接泄漏,超过一定时间后连接池应该主动把其回收。怎么实现这些的功能,这里就不在一一说明,大家独自思考一下应该怎么实现这些功能。

3 0

时间: 2024-10-07 01:32:50

Qt数据库之数据库连接池的相关文章

Qt数据库之数据库连接池-转自网络

在前面的章节里,我们使用了下面的函数创建和取得数据库连接: void createConnectionByName(const QString &connectionName) { QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL", connectionName); db.setHostName("127.0.0.1"); db.setDatabaseName("qt"); // 如

关于SQLSERVER数据库连接池

页内导航 1.如何开启连接池? 2. 那连接池是和什么有关呢? 3.如何使用相同的连接池访问不同的数据库? 4.数据库连接池的默认最大和最小值 ‘关于数据库连接池大家都听说过或者用过,但真正的了解有多少呢? 数据连接池如何启用?有哪些主要的参数? 为什么要使用连接池? 如何关闭连接池? 如何在不开启新的连接池情况下切换当前数据库? 连接池的生命周期? 当数据库服务器强制关闭连接时会怎么样? =====================================================

Myeclipse WEB工程JSP使用JNDI 数据库连接池连接Mysql数据库

在网上查了很多,最后实现了.下面写一下过程: 首先,在WEBROOT/META-INF下建一个文件context.xml,内容为: <?xml version="1.0" encoding="UTF-8"?> <Context> <Resource name="jdbc/ConnectionPool" auth="Application" type="javax.sql.DataSour

MVC设计模式((javaWEB)在数据库连接池下,实现对数据库中的数据增删改查操作)

设计功能的实现: ----没有业务层,直接由Servlet调用DAO,所以也没有事务操作,所以从DAO中直接获取connection对象 ----采用MVC设计模式 ----采用到的技术 .MVC设计模式,JSP,Servlet,POJO .数据库使用mysql .数据库连接池需要使用C3P0数据库连接池 .页面上的提示需要使用jQuery ----技术难点 .多个请求如何使用一个Servlet .如何模糊查询 .如何在创建和修改的情况下,验证用户信息是否已被使用,并给出提示 ---------

Mybatis-update - 数据库死锁 - 获取数据库连接池等待

最近学习测试mybatis,单个增删改查都没问题,最后使用mvn test的时候发现了几个问题: update失败,原因是数据库死锁 select等待,原因是connection连接池被用光了,需要等待 get: 要勇于探索,坚持就是胜利.刚看到错误的时候直接懵逼,因为错误完全看不出来,属于框架内部报错,在犹豫是不是直接睡觉得了,毕竟也快12点了.最后还是给我一点点找到问题所在了. 同上,要敢于去深入你不了解的代码,敢于研究不懂的代码. 距离一个合格的码农越来越远了,因为越学越觉得漏洞百出,自己

项目重构之数据源配置与优化:log4j 配置数据库连接池Druid,并实现日志存储到数据库

作者:泥沙砖瓦浆木匠 个人签名:打算起手不凡写出鸿篇巨作的人,往往坚持不了完成第一章节. 如果我的帮到了你,是否乐意捐助一下或请一杯啤酒也好呢?有你支持,干的更好~ 点这参与众筹 我的支付宝:13958686678 一. 前言 泥瓦匠又和大家见面了,最近两天我在Code Review ,顺便代码小小的Refactoring(重构)下.先了解这个项目吧,这次解决的是数据源配置优化.因为这web项目中配置数据源的地方很多.例如JDBC要配置数据源,Mybatis要配置数据源,Quartz定时任务要配

DBCP数据库连接池——可适用DB2数据库

前提:     项目导入DB2的驱动jar包 驱动包 下载 >关于DBCP DBCP(DataBase connection pool),数据库连接池.是 apache 上的一个 java 连接池项目,也是 tomcat 使用的连接池组件.单独使用dbcp需要3个包:commons-dbcp.jar,commons-pool.jar,commons-collections.jar由于建立数据库连接是一个非常耗时耗资源的行为,所以通过连接池预先同数据库建立一些连接,放在内存中,应用程序需要建立数据

数据库连接池应用中数据库服务器断开超时连接的问题

数据库应用开发过程中,我们可能会遇到一个问题:应用使用了数据库连接池,每经过指定时间后,发出到数据库服务器的任何请求都会失败,而且有且仅有一次失败,之后的正常访问都没有问题.尤其是在Web应用中,如果晚上时段没有访问,而第二天第一个访客的经历就是碰到一个数据库访问错误,如果开发系统的程序员没有注意这个问题的话,可能终端用户访问会看到抛出的一堆数据库异常信息. 其实,这个问题的主要原因是,应用中数据库连接池中会保存指定数量的数据库连接实例,而这些连接实例并没有定时地检测其到数据库服务器连接是否正常

SQL数据库优化:切割、数据库连接池--【考试系统】

上篇讲到了考试过程中,开发人员需要关注cpu和内存.sql日志也不容忽视,sql日志中显示了数据库操作系统的报错日志,给排错提供了很大的便利. 考试的数据库中写入了一些监听死锁和当前最耗资源语句的SQL语句.可以及时的监控死锁和了解当前考试进行到哪一步,是抽题,还是答题,还是交卷. 其中,听到了两个词:切割.数据库连接池. 后来查了一下,发现这两种方法都可以从不同的程度上对数据库的性能进行优化. 一.切割 横向切割: 就是把行分类,常用的两种是按照时间.索引划分.时间划分:比如5年的历史数据,根