这篇文章来源于b站up主——爱编程的大丙老师的一个数据库连接池的视频。老师的视频内容做的非常好,希望大家能够去b站上多多支持老师!
这是我第一次写csdn,有什么格式和内容的错误希望大家多多包涵
基于C++11的数据库连接池 | 爱编程的大丙 (subingwen.cn)
Ubuntu下如何安装MySQL获得mysql.h 建立C接口 - 数据库 - 亿速云
Ubuntu19.04 安装 MySQL 8.0.16_Weison Wei的博客-CSDN博客
基于C++11的数据库连接池【C++/数据库/多线程/MySQL】_哔哩哔哩_bilibili
环境配置
安装mysql环境时一直报错
首先是没有mysql.h文件,安装后解决了
但是仍然报错:
参考文章:ubuntu18.04编译安装mysql驱动:QSqlDatabase: QMYSQL driver not loaded-pudn.com
解决过程:
- locate libmysqlclient.so定位到动态库的位置
- 更改cmake文件
添加LINK_DIRECTORIES( ) 和 target_link_libraries()语句
安装jsoncpp
Ubuntu 安装 jsoncpp的全过程_yes_I_am的博客-CSDN博客_ubuntu上安装使用 jsoncpp
调试方法
- 头文件加入#include <jsoncpp/json/json.h>
- 更改cmake文件
调用thread失败
#include <thread>需要pthread库,但是它并不是linux下的标准库,因此需要修改cmake文件:
项目设计
一、封装MySQL API
设计过程
- 思考数据库都需要进行什么操作
- 思考接口应该是public还是private
- 思考内存的申请与释放
// // Created by ht on 2022/10/28. // #ifndef CONNECTIONPOOL_MYSQLCONN_H #define CONNECTIONPOOL_MYSQLCONN_H #include <iostream> #include <mysql/mysql.h> using namespace std; class MysqlConn { //初始化数据库连接 MysqlConn(); //释放数据库连接 ~MysqlConn(); //连接数据库 bool connect(string user, string passwd, string dbName, string ip, unsigned port = 3306); //更新数据库:insert, update, delete bool update(string sql); //查询数据库 bool query(string sql); //遍历查询得到的结果集 bool next(); //得到结果集中的字段值 string value(int index); //事务操作 bool transaction(); //提交事务 bool commit(); //事务回滚(当提交的事务出现问题时) bool rollback(); private: MYSQL* m_conn = nullptr; //数据库查询的结果集 MYSQL_RES* m_result; //结果集的指针数组,实际上是一个char**类型 MYSQL_ROW m_row = nullptr; /* WARNNING: * 需要思考MYSQL_RES* m_result什么时候被释放 * 不同于MYSQL* m_conn在析构函数中被释放, * 每次查询完都要将m_result释放 * 因此我们需要手动将其释放——写一个释放函数 * 而MYSQL_ROW m_row指向的是m_result,因此在其被释放之后,它会被自动释放 * 因为释放函数不需要被外部调用,因此将其定义为private类型 */ void freeResult(); }; #endif //CONNECTIONPOOL_MYSQLCONN_H
MySQL API的实现
//MySQL API的封装 // Created by ht on 2022/10/28. // #include "MysqlConn.h" //构造函数 MysqlConn::MysqlConn() { m_conn = mysql_init(nullptr); //设置编码格式UTF-8,防止出现乱码 mysql_set_character_set(m_conn, "utf8"); } //析构函数 MysqlConn::~MysqlConn() { if (m_conn != nullptr) { mysql_close(m_conn); } //释放m_result freeResult(); } //调用该函数,客户端与服务端建立连接 bool MysqlConn::connect(std::string user, std::string passwd, std::string dbName, std::string ip, unsigned int port) { //调用c_str()方法,将string转换为char*类型 MYSQL* ptr = mysql_real_connect(m_conn, ip.c_str(), user.c_str(), passwd.c_str(), dbName.c_str(), port, nullptr, 0); //根据ptr是否为空指针决定返回类型 return ptr != nullptr; } //更新数据库 bool MysqlConn::update(string sql) { if (mysql_query(m_conn, sql.c_str())) { return false; } return true; } //查询数据库 bool MysqlConn::query(std::string sql) { //释放掉上次申请的m_result freeResult(); //不用担心是第一次查询,因为freeResult()函数中做了if(m_result)处理 if (mysql_query(m_conn, sql.c_str())) { return false; } //当查询成功之后需要保存结果集到客户端 m_result = mysql_store_result(m_conn); //查询到结果集之后需要对结果集进行遍历:Mysql::next() return true; } //遍历查询得到的结果集 bool MysqlConn::next() { //如果为空则没有必要遍历 if (m_result != nullptr) { m_row = mysql_fetch_row(m_result);//返回的是一个二级指针,指向的是一个一级指针的指针数组 } return false; } //得到结果集中的字段值 string MysqlConn::value(int index) { //得到字段数量(列数) int rowCount = mysql_num_fields(m_result); //判断index是否为一个有效值 if (index >= rowCount || index < 0) { //返回空的字符串 return string(); } char* val = m_row[index]; /* * 需要注意,转换得到的val中可能出现'\0'这样的字符,因此需要对val的长度进行判断 */ unsigned long length = mysql_fetch_lengths(m_result)[index]; return string(val, length); /* * 这样就不会以'/0'为结束符,而是生成length长度的string */ } //事务操作 bool MysqlConn::transaction() { return mysql_autocommit(m_conn, false); //自动提交为true,手动提交为false } //提交事务 bool MysqlConn::commit() { return mysql_commit(m_conn); } //事务回滚 bool MysqlConn::rollback() { return mysql_rollback(m_conn); } //释放m_result的内存 void MysqlConn::freeResult() { //首先需要判断m_result是否为空 if (m_result) { mysql_free_result(m_result); m_result = nullptr; } /* * 定义好函数之后需要想想,什时候我们需要释放这块内存 * 1. 在m_conn析构之后需要调用释放函数 * 2. 查询结果集之后需要调用释放函数 query()函数体请m_result之前,释放掉上次申请的m_result */ }
接口的内容比较多,但属于比较通用的接口,需要进行记录
二、服务器连接
服务器到数据库
- 三次握手——>相当于网络服务器是数据库服务器的客户端——>TCP通信
- 连接认证
- 数据库关闭时进行资源释放
- 四次挥手
当我们要进行大量的这四次操作时,将会浪费很多时间,因此引出了数据库连接池,从数据库取出连接,结束后将连接池还给连接池。
创建连接池
如何创建一个连接池?
连接池本身应该是一个对象,而且这个类应该是个单例模式的类
(单例模式,属于创建类型的一种常用的软件设计模式。通过单例模式的方法创建的类在当前进程中只有一个实例(根据需要,也有可能一个线程中属于单例,如:仅线程上下文内使用同一个实例))
这个单例模式的类应该具备什么属性?
类的属性
数据库的IP,端口,用户名,密码,数据库自身的名字dbName
如何存储这些连接?
STL中的队列
具体设计细节
- 若连接池为空,设置客户端的等待时长
1.1 选择一直等待
1.2 取消等待
- 销毁多余连接
当一个连接被创建时,为其打上时间戳,当它的存活时间超出阈值则销毁
- 多线程的操作:互斥锁
- 可以将多线程——连接池 看做一个生产者——消费者模型,连接池生产线程。
若连接池充足,则让其生产线程堵塞(对应技术点是C++11中的条件变量)
代码实现
ConnectionPool.h
//定义一个数据库连接池 // Created by ht on 2022/10/31. // #ifndef CONNECTIONPOOL_CONNECTIONPOOL_H #define CONNECTIONPOOL_CONNECTIONPOOL_H #include <queue> #include "MysqlConn.h" #include <mutex> #include <condition_variable> #include <jsoncpp/json/json.h> //读Json文件需要添加fstream库 #include <fstream> using namespace std; class ConnectionPool { public: //提供一个静态接口 static ConnectionPool* getConnectPool(); //同样的,拷贝构造函数也应该设置为私有的,或者设置为delete的,程序中就无法使用这个函数了 ConnectionPool(const ConnectionPool& obj) = delete; //赋值操作符也应该delete ConnectionPool& operator=(const ConnectionPool& obj) = delete; private: //我们需要让构造函数不能在外部被调用,因此设置为私有 ConnectionPool(); //用队列实现连接池 queue<MysqlConn*> m_connectionQ; //连接的属性信息 //ip地址 string m_ip; //服务器用户名 string m_user; //服务器密码 string m_passwd; //要访问数据库的名字 string m_dbName; //访问端口 unsigned short m_port; //连接的上限 int m_maxSize; //连接的下限 int m_minSize; /* * 为了实现销毁的操作,我们需要为每一个连接设置等待时长 */ int m_timeout; //等待一个m_timeout的时间 int m_maxIdleTime; //ms /* * 考虑共享资源的问题,添加互斥锁,防止资源混乱,这里采用mutex */ mutex m_mutexQ; /* * 考虑生产者-消费者模型,当数据库生产满时,和数据库为空时, * 都需要堵塞进程,这里的技术实现是条件变量, * C++11中提供了两种条件变量 */ condition_variable m_cond; /* * 我们定义了这么多属性,需要配置文件为这些属性赋值,也需要接口函数调用配置文件 * 由于不需要将接口暴露给使用者,因此我们将其定义为private的 */ bool parseJsonFile(); }; #endif //CONNECTIONPOOL_CONNECTIONPOOL_H
Connection.cpp
// // Created by ht on 2022/10/31. // #include "ConnectionPool.h" //导入Json命名空间 using namespace Json; ConnectionPool* ConnectionPool::getConnectPool() { /* * Description: * 在C++11中,static类型是线程安全的,因此定义static类型的静态局部对象 */ static ConnectionPool pool; /* * Description: * 因为他是static类型,因此它的生命周期很长,但是访问权限仅限于getConnectPool()函数中 */ //通过这种方式可以得到当前对象的一个实例 return &pool; } //若读出了正确的数据则返回true bool ConnectionPool::parseJsonFile() { ifstream ifs("dbconf.json"); //读入"dbconf.json"文件 //定义Reader对象 Reader rd; //定义一个传出对象,接受rd.parse()的结果 Value root; rd.parse(ifs, root); //判断传入的是否为一个Json对象 if (root.isObject()) { //若是Json对象,则把配置文件中的数据依次读出 m_ip = root["ip"].asString(); m_ip = root["port"].asInt(); m_user = root["userName"].asString(); m_passwd = root["dbName"].asString(); m_minSize = root["minSize"].asInt(); m_maxSize = root["maxSize"].asInt(); m_maxIdleTime = root["maxIdleTime"].asInt(); m_timeout = root["timeout"].asInt(); } }
我们为数据库服务器定义了许多属性,这些属性可以被写进配置文件中,我们采用Json文件进行配置,并定义Json文件的接口ParseJsonFile()
json文件:
dbconf.json
{ "ip": "192.168.237.131", "port": 3306, "userName": "root", "password": "root", "dbName": "testdb", "minSize": 100, "maxSize": 1024, "maxIdleTime": 5000, "timeout": 1000 }
至此已经可以实现构造函数
数据库连接池实现
数据库连接池构造函数
//构造函数的实现 ConnectionPool::ConnectionPool() { //加载Json配置文件里的数据来初始化类内成员 if (!parseJsonFile()) { return ; } //创建m_minSize个数的连接 for (int i = 0; i < m_minSize; i++) { //实例化一个MysqlConn类型的对象 //tips:一开始没有写public,因此一直报错,因为class会自动定义成private MysqlConn* conn = new MysqlConn; conn->connect(m_user, m_passwd, m_dbName, m_ip, m_port); //将连接放进队列m_connectionQ m_connectionQ.push(conn); } /* * 创建两个线程类,控制连接的销毁与创建 * 如果需要让这两个线程工作,需要创建任务函数 * 这个任务函数有几种类型 * 1. 指定一个有名的函数,它可以属于类也可以不属于类 * 2. 指定一个匿名函数,即lambda表达式 * 3. 指定类的静态成员函数 * 4. 指定类的非静态成员函数 * 5. 指定一个可调用对象——仿函数(实质也是一个类,不过这个类重载了‘()’运算符) */ //生产连接线程,注意这个线程调用函数的接口内容是一个函数的地址和当前类的实例对象this thread producer(&ConnectionPool::produceConnection, this); //查看是否有需要销毁的连接 thread recycler(&ConnectionPool::recycleConnection, this); //对于这两个线程,选择当前类的非静态成员函数 /* * 这里的两个线程如果直接操作的话会阻塞主线程,但是主线程里面还需要进行其他的操作 * 因此我们不能通过producer或者recycler对象调用join方法,如果调用,则会调用主线程 * 我们可以让主线程与子线程分离,产生两个子线程对象,使用detach()函数 */ producer.detach(); recycler.detach(); }
构造函数的实现过程:
- 调用parseJsonFile()函数,加载配置信息
- 创建m_minSize个数的连接
- 把这些连接插入队列当中
- 创建两个线程,分别进行生产连接producer()和销毁连接recycler()
- 将这两个子线程与主线程分离,避免阻塞主线程
注意thread函数的调用方法——(函数的引用,函数所属对象)
生产连接线程与销毁连接线程
//生产连接线程,注意这个线程调用函数的接口内容是一个函数的地址和当前类的实例对象this thread producer(&ConnectionPool::produceConnection, this); //查看是否有需要销毁的连接 thread recycler(&ConnectionPool::recycleConnection, this);
这两个线程需要两个函数进行操作:
//生产连接函数 void produceConnection(); //回收连接函数 void recycleConnection(); //添加连接函数 void addConnection();
为了提升代码的复用性,将构造函数中重复创建连接的部分写成一个函数
void ConnectionPool::addConnection() { //实例化一个MysqlConn类型的对象 //tips:一开始没有写public,因此一直报错,因为class会自动定义成private MysqlConn* conn = new MysqlConn; conn->connect(m_user, m_passwd, m_dbName, m_ip, m_port); //连接被创建以后,记录时间戳 conn->refreshAliveTime(); //将连接放进队列m_connectionQ m_connectionQ.push(conn); }
生产连接线程 produceConnection()
void ConnectionPool::produceConnection() { //持续创建一个线程 while (true) { unique_lock<mutex> locker(m_mutexQ); //unique_lock<mutex>对象可以自动对m_mutexQ加锁和解锁 while (m_connectionQ.size() >= m_minSize) //阻塞线程的条件 { m_cond.wait(locker); } //若小于m_minSize,则进行addConnection() /* * tips: * 用while进行判断,而不是if的原因: * 设想这种情况,当有两个生产者线程被唤醒,且此时m_connectionQ队列还差一个元素就满了 * 若是if()判断,则会让队列溢出,while循环则会重复进行判断 * 但是我们这里生产数据库的线程只有一个,因此不会出现这种情况,但需要考虑多个生产线程的情况 */ addConnection(); } }
回收连接线程 recycleConnection()
void ConnectionPool::recycleConnection() { //判断原则是根据空闲时长 /* * 空闲时间的计算思路 * 应该计算当前时刻 - 该连接被还回来的时刻 * 计算函数应当被放在MysqlConn类当中,这属于数据库操作的API */ while (true) { //不能持续进行这个线程,让其定时休眠1s this_thread::sleep_for(chrono::seconds(1)); while (m_connectionQ.size() > m_minSize) { MysqlConn* conn = m_connectionQ.front(); if (conn->getAliveTime() >= m_maxIdleTime) { m_connectionQ.pop(); delete conn; } else break; } } }
连接空闲时长的计算
这个计算过程应该属于Mysql的API,因此将其封装进MysqlConn类中
//时钟对应头文件 #include <chrono> using namespace chrono; //刷新起始的时间点 void MysqlConn::refreshAliveTime() { m_alivetime = steady_clock::now(); } //计算连接存活的总时长 long long MysqlConn::getAliveTime() { nanoseconds res = steady_clock::now() - m_alivetime; //res的时间精度是纳秒,将其变成毫秒 milliseconds millsec = duration_cast<milliseconds>(res); //调用count()就可以计算出一共有多少毫秒 return millsec.count(); }
数据库连接的获取和回收
定义获取连接的函数
初级版本:
MysqlConn* ConnectionPool::getConnection() { //判断连接池中的队列是否为空,若为空则阻塞线程 //定义一个互斥锁 unique_lock<mutex> locker(m_mutexQ); //判断队列是否为空,若为空则等待(阻塞)m_timeout的时间,然后再次判断是否为空 while (m_connectionQ.empty()) { if (cv_status::timeout == m_cond.wait_for(locker, chrono::milliseconds(m_timeout))) { if (m_connectionQ.empty()) { continue; } } } //若不为空,取出可用连接 MysqlConn* conn = m_connectionQ.front(); m_connectionQ.pop(); return conn; }
先判断队列是否为空,若为空则堵塞进程,若不为空则取出连接。
升级版本:
我们在使用一个连接之后,想要将其归还给连接池,可以采用智能指针实现。
shared_ptr<MysqlConn> ConnectionPool::getConnection() { //判断连接池中的队列是否为空,若为空则阻塞线程 //定义一个互斥锁 unique_lock<mutex> locker(m_mutexQ); //判断队列是否为空,若为空则等待(阻塞)m_timeout的时间,然后再次判断是否为空 while (m_connectionQ.empty()) { if (cv_status::timeout == m_cond.wait_for(locker, chrono::milliseconds(m_timeout))) { if (m_connectionQ.empty()) { continue; } } } //若不为空,取出可用连接 shared_ptr<MysqlConn> connptr(m_connectionQ.front()); m_connectionQ.pop(); return connptr; }
但是,智能指针是可以自动进行资源的回收,但是和我们归还连接的需求不相符,那么应该如何实现我们的需求?
构造一个删除器
若连接池不为空,取出可用连接,赋值给connptr
为connptr赋值分为两步,第一步时传入队列的头部元素
第二步是定义一个删除器,我们在这里将这个删除器写成一个匿名函数的形式,删除器首先给线程加锁,然后更新时间戳、
将元素压入队列中,即是完成了连接归还到连接池的操作
shared_ptr<MysqlConn> connptr(m_connectionQ.front(), [this](MysqlConn* conn){ //对当前线程加锁 lock_guard<mutex> locker(m_mutexQ); //lock_guard在locker析构时会自动解锁 //更新时间戳 conn->refreshAliveTime(); m_connectionQ.push(conn);
实际上,我们对一个线程进行堵塞操作后,需要人为的将其唤醒,唤醒的方式是通过条件变量:condition_variable m_cond;
我们这里的设计模式是生产者——消费者模式,
生产者是void ConnectionPool::produceConnection()
消费者是shared_ptr<MysqlConn> ConnectionPool::getConnection()
在getConnection()中我们判断了m_connectionQ.empty(),若为空,则堵塞进程,而这时我们的produceConnection()
会不断地生产连接,生产过后我们应该唤醒进程,那么条件变量就起了作用
在ConnectionPool::produceConnection()中添加
在addConnection()后,唤醒getConnection()进程
而在消费者取出了队列中的连接过后,我们就唤醒produceConnection()进程
在ConnectionPool::getConnection()中添加
这里我们用的notify_all()函数,会让生产者和消费者同时被唤醒,但是不用担心,因为我们的判断是基于while()的,在不满足条件时会重复进行判断
例如我们唤醒了消费者进程后,
如果m_connectionqQ为空,那么就还是会被阻塞
我们这里生产者与消费者共用一个条件变量m_cond,但是如果代码比较复杂,需要把生产者与消费者的条件变量分开,比如生产者的生产数量有上限时
数据库连接池的资源释放和互斥锁的检查
连接如果要被释放,那么被释放的应该是一块堆内存,但是我们把queue<MysqlConn*> m_connectionQ 释放时,释放的只是它的指针,而它的地址也应该被释放。
因此我们在class ConnectionPool类的析构函数中将队列中存储的连接全部析构掉。
//析构函数 ConnectionPool::~ConnectionPool() { while (!m_connectionQ.empty()) { MysqlConn* conn = m_connectionQ.front(); m_connectionQ.pop(); delete conn; } }
下面检查一下,在访问共享资源时,是否有冲突的情况,如果有我们需要添加互斥锁。
- 第一个函数:ConnectionPool* ConnectionPool::getConnectPool()
static ConnectionPool pool中,static类型是线程安全的,因此不用担心
- 第二个函数:bool ConnectionPool::parseJsonFile()
仅仅是读取了数据,因此也不用加锁
- 第三个函数:void ConnectionPool::produceConnection()
我们已经为其加锁了
unique_lock<mutex> locker(m_mutexQ);
- 第四个函数:void ConnectionPool::recycleConnection()
这个函数的目的是销毁空余连接
但是我们并没有为它加锁,这里对其修改
void ConnectionPool::recycleConnection() { //判断原则是根据空闲时长 /* * 空闲时间的计算思路 * 应该计算当前时刻 - 该连接被还回来的时刻 * 计算函数应当被放在MysqlConn类当中,这属于数据库操作的API */ while (true) { //不能持续进行这个线程,让其定时休眠1s this_thread::sleep_for(chrono::seconds(1)); while (m_connectionQ.size() > m_minSize) { MysqlConn* conn = m_connectionQ.front(); if (conn->getAliveTime() >= m_maxIdleTime) { m_connectionQ.pop(); delete conn; } else break; } } }
修改位置:
自动对这个进程加锁,在while()循环结束之后自动解锁
- 第五个函数:void ConnectionPool::addConnection()
这个函数的内部是不用加锁的,应该是在调用这个函数的时候再进行加锁。
- 第六个函数shared_ptr<MysqlConn>ConnectionPool::getConnection()
这个函数的加锁操作我们已经进行过了。
测试阶段
第一步,构建测试框架
// // Created by ht on 2022/10/28. // #include <iostream> #include <memory> #include "MysqlConn.h" #include "ConnectionPool.h" using namespace std; int query() { //创建连接池对象 MysqlConn conn; } int main() { return 0; }
第二步,连接MySQL服务器
一直连接不上去,参考文章解决
Mysql:ERROR 1698 (28000): Access denied for user 'root'@'localhost' - 腾讯云开发者社区-腾讯云
进入指令:mysql -u root -p
密码:root
或者输入:mysql -uroot -proot
常用数据库操作
# 建库 create database mysql_shiyan # 建表 create table department(id int, name varchar(20), phone int); # 插入数据 insert into department(id, name, phone) values(01, 'Tom','1316631327'); insert into department(id, name, phone) values(02, 'jerry','2020'); # 删库 drop database shiyan # 删表 drop table department # 主键 create table user(id int primary key, name varchar(20)); # 自增 create table user(id int primary key auto_increment, name varchar(20)); //重命名一张表 rename table old_table to new_table; alter table odl_table rename new_table;
查看数据库 :show databases;
创建数据库:create database testdb;
使用数据库:use testdb;
查看数据库中的数据表:show tables;
创建数据表:create table ......
标准格式:
oot@host# mysql -u root -p Enter password:******* mysql> use RUNOOB; Database changed mysql> CREATE TABLE runoob_tbl( -> runoob_id INT NOT NULL AUTO_INCREMENT, -> runoob_title VARCHAR(100) NOT NULL, -> runoob_author VARCHAR(40) NOT NULL, -> submission_date DATE, -> PRIMARY KEY ( runoob_id ) -> )ENGINE=InnoDB DEFAULT CHARSET=utf8; Query OK, 0 rows affected (0.16 sec) mysql>
插入数据:insert into person(id, age, sex, name) values(1, '20', 'woman','nami');
查看数据表有哪些记录:select * from person
第三步,编写连接函数
调用该函数,客户端与服务端建立连接
bool MysqlConn::connect(std::string user, std::string passwd, std::string dbName, std::string ip, unsigned int port)
第四个参数是std::string ip
我们需要查询一下当前的ip地址:
代码为:
//连接数据库 conn.connect("root", "root", "testdb", "127.0.0.1", 3306);
返回布尔值为1,连接成功。
第四步,插入sql语句
//插入sql语句 string sql = "insert into person values(5, 25, 'man', 'tom')"; //执行sql语句 bool flag = conn.update(sql); cout << "是否执行成功?" << flag << endl;
显示插入成功
但是在程序中进行查询操作失败了
没有输出结果
单线程模式下测试连接池与非连接池
测试前清空数据库:
单线程模式不使用连接池
//单线程不使用连接池 void op1(int begin, int end) { for (int i = begin; i < end; i++) { //连接、插入 //注意这里创建conn对象一定要写在循环体里面,不然只能创建一次对象 MysqlConn conn; conn.connect("root", "root", "testdb", "127.0.0.1", 3306); //插入sql语句 string sql = "insert into person values(5, 25, 'man', 'tom')"; //对原始的sql语句进行拼接操作,使得每个插入的id不同 char sql[1024] = {0}; sprintf(sql, "insert into person values(%d, 25, 'man', 'tom')", i); //执行sql语句 conn.update(sql); } }
单线程使用数据库连接池
//单例模式使用数据库连接池 void op2(ConnectionPool* pool, int begin, int end) { for (int i = begin; i < end; i++) { //getConnection()返回的是一个智能指针类型 shared_ptr<MysqlConn> conn = pool->getConnection(); char sql[1024] = {0}; sprintf(sql, "insert into person values(%d, 25, 'man', 'tom')", i); //执行sql语句 conn->update(sql); } }
单线程模式测试函数
void test1() { #if 0 //非数据库连接池接模式 steady_clock::time_point begin = steady_clock::now(); op1(0, 5000); steady_clock::time_point end = steady_clock::now(); auto length = end - begin; //注意这里length是duration类型,输出时要用count()计数 cout << "单线程模式下非连接池用时:" << length.count() << "纳秒" << length.count() / 1000000 << "毫秒" << endl; #else //数据库连接池模式 ConnectionPool* pool = ConnectionPool::getConnectPool(); steady_clock::time_point begin = steady_clock::now(); op2(pool, 0, 5000); steady_clock::time_point end = steady_clock::now(); auto length = end - begin; cout << "单线程模式下连接池用时:" << length.count() << "纳秒" << length.count() / 1000000 << "毫秒" << endl; #endif }
运行时一直有bug:
无论我怎么运行,数据库都只能插入一条数据,但是我设定的值为5000条
debug时发现,析构函数被调用之后,循环会自动结束
即不会重新进入for循环,这让我觉得是否是因为析构后会自动跳出循环。
在看代码时我发现,m_result好像一直没有被调用,且没有给初值,并且MysqlConn的构造函数有下划线,它也显示出了有变量未被初始化。
//数据库查询的结果集 MYSQL_RES* m_result = nullptr;
m_result赋初值后,发现程序回归了正常。
结果:
单线程模式下非连接池用时:35688390914纳秒 35688毫秒
但是目前单线程下连接池模式还是存在问题,怀疑Json文件的读取有问题,经过一番搜索,发现是Json文件的位置放错了
debug过程:
编写一个JsonTest()函数
void JsonTest() { Value root; root["ip"] = "127.0.0.1"; root["port"] = 3306; //序列化 FastWriter w; string json = w.write(root); ofstream ofs("test.json"); ofs << json; ofs.close(); }
这段代码会输出一个Json文件,但是当前文件夹中我并没有找到生成的test.json文件,而是在cmake文件夹下发现了
当我把testdb.json文件也放在该目录下,编译通过了
结果:
单线程模式下连接池用时:4546899261纳秒 4546毫秒
多线程模式测试函数
//多线程模式下测试函数 void test2() { #if 0 MysqlConn conn; conn.connect("root", "root", "testdb", "127.0.0.1"); steady_clock::time_point begin = steady_clock::now(); /* * 添加线程操作 * t1会执行1000次op1操作 */ thread t1(op1, 0, 1000); thread t2(op1, 1000, 2000); thread t3(op1, 2000, 3000); thread t4(op1, 3000, 4000); thread t5(op1, 4000, 5000); t1.join(); t2.join(); t3.join(); t4.join(); t5.join(); steady_clock::time_point end = steady_clock::now(); auto length = end - begin; //注意这里length是duration类型,输出时要用count()计数 cout << "多线程模式下非连接池用时:" << length.count() << "纳秒 " << length.count() / 1000000 << "毫秒" << endl; #else steady_clock::time_point begin = steady_clock::now(); /* * 添加线程操作 * t1会执行1000次op1操作 */ ConnectionPool* pool = ConnectionPool::getConnectPool(); thread t1(op2, pool, 0, 1000); thread t2(op2, pool, 1000, 2000); thread t3(op2, pool, 2000, 3000); thread t4(op2, pool, 3000, 4000); thread t5(op2, pool, 4000, 5000); t1.join(); t2.join(); t3.join(); t4.join(); t5.join(); steady_clock::time_point end = steady_clock::now(); auto length = end - begin; //注意这里length是duration类型,输出时要用count()计数 cout << "多线程模式下连接池用时:" << length.count() << "纳秒 " << length.count() / 1000000 << "毫秒" << endl; #endif }
操作方法:
手动添加五个线程
thread t1(op1, 0, 1000);
thread t2(op1, 1000, 2000);
thread t3(op1, 2000, 3000);
thread t4(op1, 3000, 4000);
thread t5(op1, 4000, 5000);
这条语句的含义是,启动线程t1,执行0 - 1000次op1操作
但是,在执行时却发现不会工作,这是因为我们开启的五个线程是同步工作的,同时创建连接,因此数据库会拒绝一部分的访问操作。
解决方法是,在线程开启前就创建一个连接,
MysqlConn conn;
conn.connect("root", "root", "testdb", "127.0.0.1");
这样后续的进程就不会冲突了,具体的原理我还不清楚
运行结果:
多线程模式下非连接池用时:4191854079纳秒 4191毫秒
多线程模式下连接池的检测方式:
ConnectionPool* pool = ConnectionPool::getConnectPool();
thread t1(op2, pool, 0, 1000);
thread t2(op2, pool, 1000, 2000);
thread t3(op2, pool, 2000, 3000);
thread t4(op2, pool, 3000, 4000);
thread t5(op2, pool, 4000, 5000);
在建立线程前,创建一个连接池即可,
语句的含义是,对t1进程执行op2(pool, 0 ,1000);
需要注意的是,使用连接池并不会产生线程冲突,因为每个连接都是我们采用for循环的方式生产出来并放入连接池的
运行结果:
多线程模式下连接池用时:1964814208纳秒 1964毫秒
通过解析代码我发现,连接池生产连接的过程是for循环m_minSize次数的,
而这个m_minSize只有100次,在json文件中更改m_minSize为1000,结果变为:
多线程模式下连接池用时:754137643纳秒 754毫秒