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-1Alice
In thread run():Connection-2Alice
In thread run():Connection-3Alice
In thread run():Connection-4Alice
In thread run():Connection-5
Test connection on borrow, execute:SELECT 1, forConnection-1Alice
In thread run():Connection-1
Test connection on borrow, execute:SELECT 1, forConnection-2Alice
In thread run():Connection-2
Test connection on borrow, execute:SELECT 1, forConnection-3Alice
In thread run():Connection-3
Test connection on borrow, execute:SELECT 1, forConnection-4Alice
In thread run():Connection-4
Test connection on borrow, execute:SELECT 1, forConnection-5Alice
In thread run():Connection-5Alice
可以看到,前 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 里面,即连接泄漏,超过一定时间后连接池应该主动把其回收。怎么实现这些的功能,这里就不在一一说明,大家独自思考一下应该怎么实现这些功能。
Qt数据库之数据库连接池的更多相关文章
- java连数据库和数据库连接池踩坑日记(一)-------oracle连接的一些问题
最近接触oracle有点多,同时也在配置数据库连接池,坑也就踩多了,记录下. 事情还没有结束,没时间记录问题,很多事情都忘了,过了国庆再写的话可能就真的全忘了吧……而且不单单是数据库问题,还有一些数据 ...
- java连数据库和数据库连接池踩坑日记(二)-------数据库连接池c3p0
关于数据库连接池,我觉得有些沮丧,因为最后被毙掉了说不用考虑多线程的问题…… 数据库连接池的推荐:https://www.cnblogs.com/nuccch/p/8120349.html 我最终选择 ...
- java web中jsp连接mysql数据库 以及数据库连接池的使用
将mysql-connector-java-5.1.6-bin.jar导入到tomcat的lib目录下. 在java项目中,只需要引入mysql-connector-java-5.1.6-bin.ja ...
- 关于SQLSERVER数据库连接池
页内导航 1.如何开启连接池? 2. 那连接池是和有什么有关呢? 3.如何使用相同的连接池访问不同的数据库? ‘关于数据库连接池大家都听说过或者用过,但真正的了解有多少呢? 数据连接池如何启用?有哪些 ...
- 如何用C++自己实现mysql数据库的连接池?
为什么是mysql? 现在几乎所有的后台应用都要用到数据库,什么关系型的.非关系型的:正当关系的,不正当关系的:主流的和非主流的, 大到Oracle,小到sqlite,以及包括现在逐渐流行的基于物联网 ...
- Mybatis-update - 数据库死锁 - 获取数据库连接池等待
最近学习测试mybatis,单个增删改查都没问题,最后使用mvn test的时候发现了几个问题: update失败,原因是数据库死锁 select等待,原因是connection连接池被用光了,需要等 ...
- MVC设计模式((javaWEB)在数据库连接池下,实现对数据库中的数据增删改查操作)
设计功能的实现: ----没有业务层,直接由Servlet调用DAO,所以也没有事务操作,所以从DAO中直接获取connection对象 ----采用MVC设计模式 ----采用到的技术 .MVC设计 ...
- 银行账户管理系统(oracle数据库连接池,数据库的链接,)
/* * 银行账户管理系统: * 属性:账户id,姓名,金额salary,利息类型: *管理员模块实现的功能: * 1.给用户开户 * 2.查询所有账户信息 * 用户模块实现的功能: * 1.显示用户 ...
- Application, JDBC, 数据库连接池, Session, 数据库的关系
RT,这几个东东已经困扰我很长一段时间了... 这次争取把她们理清楚了! 参考资料: 1. 数据库连接池:http://www.cnblogs.com/shipengzhi/archive/2011/ ...
随机推荐
- 关于chrome请求被挂起页面加载缓慢问题的追查
请参考FEX团队探究结果 http://fex.baidu.com/blog/2015/01/chrome-stalled-problem-resolving-process/ 结论如下: 请求成功构 ...
- 092、部署Graylog日志系统(2019-05-16 周四)
参考https://www.cnblogs.com/CloudMan6/p/7808708.html Graylog 是与 ELK 可以相提并论的一款几种式日志管理方案,支持数据收集.检索.可视化 ...
- wex5 如何写后台BAAS
Data.java: 在class中链接数据源: 配置的numsql数据源 private static final String DATASOURCE_NUMYSQL = "numysql ...
- wex5 如何利用 百度地图 定位 和 天气插件
引包: require("cordova!cordova-plugin-geolocation"); require("cordova!com.justep.cordov ...
- linux复习3:linux字符界面的操作
一.前言 1.对linux服务器进行管理的时候,经常要进入字符界面进行操作,使用命令需要记住该命令的相关选项和参数.vi编辑器可以用于编辑任何ASCII文本,功能非常的强大,可以对文本进行创建.查找. ...
- 利用wampserve搭建本服务器
1.官网下载安装包 注意:3.0.6版本需要下载依赖包vc依赖包 2.默认为英文 右击图标进入langue设置为中文 3.需要手动设置在现状态 右击=>选中wampsetting =>me ...
- python-ssh-远程服务器+远程docker执行命令
在python语言中实现远程服务器执行命令+远程dcoker执行命令 def ssh_exec_command(ip, username, password, cmd=None): "&qu ...
- 【TensorFlow探索之一】MNIST的初步尝试
最近在学习TensorFlow,尝试的第一个项目是MNIST.首先给出源码地址. 1 数据集的获取 我们可以直接运行下面的代码,来获取到MNIST的数据集. from tensorflow.examp ...
- PAT Basic 1076 Wifi密码 (15 分)
下面是微博上流传的一张照片:“各位亲爱的同学们,鉴于大家有时需要使用 wifi,又怕耽误亲们的学习,现将 wifi 密码设置为下列数学题答案:A-1:B-2:C-3:D-4:请同学们自己作答,每两日一 ...
- 洛谷P2401 不等数列 题解
可食用的题目链接 题解: 有题目得:这个题有巧做法而不是暴力模拟.废话 这个题看着像一道dp,因为可以由前一种(数据更小的推出数据更大的)推出后一种. 我们设已经得到了n-1个数的总方法(1~n-1) ...