【C++实现】 数据库连接池
admin
2024-01-22 16:43:52
0

文章目录

  • 涉及知识
  • 为什么要弄连接池
  • 功能介绍
  • 成员变量讲解
  • 代码剖析
    • Connection.h
    • Connection.cpp
    • ConnectionPool.h
    • ConnectionPool.cpp
  • 性能测试
    • 难点
  • 总结


涉及知识


MySQL数据库编程、单例模式、queue队列容器、C++11多线程编程、线程互斥、线程同步通信和
unique_lock、基于CAS的原子整形、智能指针shared_ptr、lambda表达式、生产者-消费者线程模型

为什么要弄连接池


因为mysql 简历连接的时候需要底层先建立连接,说白了TCP需要三次握手吧,Mysql Server连接需要认证吧,Mysql Server 需要关闭连接回收资源,TCP需要四次挥手吧。
所以就是说,如果我们如果需要连接上数据库然后发送一条数据,需要做上面的这些工作。

在市场上比较流行的连接池包括阿里的druid,c3p0以及apache dbcp连接池,它们对于短时间内大量的数据库增删改查操作性能的提升是很明显的,但是它们有一个共同点就是,全部由Java实现的。

那么我们C++的也不必眼馋,自己动手实现基于C++代码的数据库连接池模块。

功能介绍


连接对象模块
Connection模块,这个模块负责实现连接mysql的部分,进行数据库增删查改接口的封装。

  • Connection初始化连接对象
  • connect 绑定ip,端口,用户,密码,库名称
  • update 传入string sql,进行实施
  • query 进行select查询,这里用不到
  • refreshAliveTime 设置每一个连接对象在队列里面呆着的起始时间
  • getAliveTime 获取对象从refreshAliveTime到如今的时间,单位ms

连接池模块模块
这个模块是一个单例对象ConnectionPool,负责封装一个个Connection。

  • 提供getConnection给外部,返回一个连接对象。
  • 内部实现produceConnectionTask,会多开一个线程检测连接对象,适当的进行扩容;
  • 内部实现 scannerConnectionTask,会多开一个线程,若是有超过初始化的连接对象长时间没有使用,就会进行释放。
  • 内部实现loadConfigFile ,会对mysql.ini配置文件进行读取,我们可以将各种信息记录到文件中,初始化连接池会进行调用读取。

成员变量讲解


ConnectionPool中会记录连接的库的ip地址,端口号,mysql用户名,密码,数据库名称; 以及 mysql初始连接数量mysql最大连接数量每一个连接对象在队列所能待的最大空闲时间获取连接的一个超时时间存储连接对象的队列保证互斥的锁条件变量总共有多少个连接池对象。

解释一下变量:
1.这里的_maxIdletime,_initSize,_maxSize,scannerConnectionTask就是通过这两个变量进行判断是否需要进行删减连接池中的连接对象。_maxIdletime标识的就是一个连接对象在队列中能呆着的最长时间。
2._connectionTimeout 是我们上层调用底层的连接池获取一个连接对象的最长等待时间。假如底层的连接对象都被获取了,那么我的线程就会在等待_connectionTimeout进行timeout一次。
3._connectionQue,_queueMutex,_cv 就是对临界资源的保护,生产者需要通过_cv确认是否需要添加新的连接对象入队列。消费者需要_cv判断此时队列是否还有连接对象可以被消费。
4._connectionCnt表示总共创建的连接对象。由于_connectionQue的大小只能说明此时有多少个连接对象还没有被使用,我们需要_connectionQue标识已经创建了多少的连接对象。_connectionQue是用来进行scannerConnectionTask,produceConnectionTask衡量的变量。

string _ip; // mysql ip地址
unsigned short _port; // mysql 端口号
string _username; // mysql 用户名
string _password; // mysql 密码
string _dbname; // 数据库名称
int _initSize;	 // mysql 初始连接量
int _maxSize;	 // mysql的最大连接量
int _maxIdletime;// 最大空闲时间
int _connectionTimeout; // 超时时间queue _connectionQue; // 存储mysql链接的队列
mutex _queueMutex; // 维护连接队列线程安全的互斥锁
condition_variable _cv; // 队列条件变量
atomic_int _connectionCnt; // 记录连接所创建的connection的总量

代码剖析

Connection.h


#define _CRT_SECURE_NO_WARNINGS
#pragma once
#include
using std::string;
#include
using std::cout;
using std::endl;#define LOG(str) \cout << __FILE__ << ":" << __LINE__ << " " << \__TIMESTAMP__ << " : " << str << endl;class Connection
{
public:// 初始化数据库连接Connection();// 释放数据库连接资源~Connection();// 连接数据库bool connect(string ip, unsigned short port, string user, string password,string dbname);// 更新操作 insert、delete、updatebool update(string sql);// 查询操作 selectMYSQL_RES* query(string sql);// 刷新一下链接的时间void refreshAliveTime(){_alivetime = clock();}clock_t getAliveTime(){return clock();}
private:MYSQL* _conn; // 表示和MySQL Server的一条连接clock_t _alivetime; // 存活时间
};

Connection.cpp


这份Connection.cpp实际上就是对sql常用的功能的封装。


#include"Connection.h"
#include// 初始化数据库连接
Connection::Connection()
{_conn = mysql_init(nullptr);
}// 释放数据库连接资源
Connection::~Connection()
{if (_conn != nullptr)mysql_close(_conn);
}
// 连接数据库
bool Connection::connect(string ip, unsigned short port, string user, string password,string dbname)
{MYSQL* p = mysql_real_connect(_conn, ip.c_str(), user.c_str(),password.c_str(), dbname.c_str(), port, nullptr, 0);return p != nullptr;
}
// 更新操作 insert、delete、update
bool Connection::update(string sql)
{if (mysql_query(_conn, sql.c_str())){LOG("更新失败:" + sql);return false;}return true;
}
// 查询操作 select
MYSQL_RES* Connection::query(string sql)
{if (mysql_query(_conn, sql.c_str())){LOG("查询失败:" + sql);return nullptr;}return mysql_use_result(_conn);
}

ConnectionPool.h


#pragma once
#include
#include
using namespace std;
#include"Connection.h"
#include
#include
#include
#include
#include
/*
实现连接池模块
*/class ConnectionPool
{
public:static ConnectionPool* getConnectionPool();// 给外部提供接口,提供一个可用的空闲连接shared_ptr getConnection();~ConnectionPool();
private:void operator=(const ConnectionPool&) = delete;ConnectionPool(const ConnectionPool&) = delete;ConnectionPool(); // 单例 构造函数私有化// 运行在独立的线程中,专门负责生产新连接void produceConnectionTask();// 扫描多余的空闲连接,超过maxIndleTimevoid scannerConnectionTask();// 从配置文件加载配置项bool loadConfigFile();string _ip; // mysql ip地址unsigned short _port; // mysql 端口号string _username; // mysql 用户名string _password; // mysql 密码string _dbname; // 数据库名称int _initSize;	 // mysql 初始连接量int _maxSize;	 // mysql的最大连接量int _maxIdletime;// 最大空闲时间int _connectionTimeout; // 超时时间queue _connectionQue; // 存储mysql链接的队列mutex _queueMutex; // 维护连接队列线程安全的互斥锁condition_variable _cv; // 队列条件变量atomic_int _connectionCnt; // 记录连接所创建的connection的总量//thread produce;//thread scanner;bool isRun = false;// 判断是否还在运行
};

ConnectionPool.cpp


#define _CRT_SECURE_NO_WARNINGS#include"ConnectionPool.h"
#include"public.h"
ConnectionPool* ConnectionPool::getConnectionPool()
{static ConnectionPool pool;return &pool;
}
ConnectionPool::~ConnectionPool()
{isRun = true;_cv.notify_all();
}// 单例 构造函数私有化
ConnectionPool::ConnectionPool()
{// 加载配置项if (!loadConfigFile()){return; // 日志信息里面有打印}// 创建初始的数量连接for (int i = 0; i < _initSize; ++i){Connection* p = new Connection();p->connect(_ip,_port,_username,_password,_dbname);p->refreshAliveTime(); // 刷新一下开始空闲的起始时间_connectionQue.push(p);_connectionCnt++;}// 启动一个新的线程,作为连接生产者,绑定有一个成员变量,并且传入this指针才能使用thread produce(std::bind(&ConnectionPool::produceConnectionTask, this));produce.detach(); 启动一个新的定时线程,扫描多余的空闲连接,超过maxIndleTimethread scanner(std::bind(&ConnectionPool::scannerConnectionTask, this));scanner.detach();
}// 扫描多余的空闲连接,超过maxIndleTime
void ConnectionPool::scannerConnectionTask()
{for (;;){if (isRun)return;// 直接睡_maxIdletime,起来就检测一次//this_thread::sleep_for(chrono::seconds(_maxIdletime));// 扫描整个队列unique_lock lock(_queueMutex);while (_connectionCnt > _initSize){if (isRun)return;// 若是每一个线程都占用着连接,此时扫描线程进来后检测到队列为空,就可以直接退出if (_connectionQue.empty()){break;}// 队头的时间是待在队列最长的Connection* p = _connectionQue.front();if (p->getAliveTime() >= _maxIdletime * 1000) // 60s 的话太长了,一般来说不会调用这里pop掉,6s的话这里会进行删除{_connectionQue.pop();_connectionCnt--;delete p; // 调用~Connection 释放连接}else{break;// 队头没有超过超时时间,那么没必要看了}}}
}// 运行在独立的线程中,专门负责生产新连接
void ConnectionPool::produceConnectionTask()
{// 生产连接需要注意不能超过最大的量for (;;){if (isRun)return;unique_lock lock(_queueMutex); // 由于wait要释放锁,所以用unique_lockwhile (!isRun && !_connectionQue.empty()){if (isRun)return;_cv.wait(lock); // 等待队列变空,此时不需要生产}if (isRun)return;// 不能访问到任何主线程的容器。// 走到这里,说明需要生产者生产连接// 若常见的连接已经比最大的创建数都多了,就不再创建了,让他们等着其他连接用完,这里补充处理if (isRun && _connectionCnt < _maxSize){// 这里是连接数量没有到达上线Connection* p = new Connection();p->connect(_ip, _port, _username, _password, _dbname);p->refreshAliveTime(); // 刷新一下开始空闲的起始时间_connectionQue.push(p);_connectionCnt++;}// 通知消费者线程可以消费,若是到达了最大值,也唤醒,因为可能有线程已经用完连接返回了_cv.notify_all();}
}
// 给外部提供接口,提供一个可用的空闲连接,消费者线程,消费者只会等待若干秒
shared_ptr ConnectionPool::getConnection()
{unique_lock lock(_queueMutex);while (_connectionQue.empty()){// 条件变量等待超时时间if (cv_status::timeout == _cv.wait_for(lock, chrono::microseconds(_connectionTimeout))){// 若果是正常返回,说明真的超时了if (_connectionQue.empty()){LOG("获取空闲连接超时了....获取连接失败!");return nullptr;}}else{} // notimeout,再检查一次}// 这里自定义删除器是因为我们不是要真正删除,而是归还到queue当中shared_ptr sp(_connectionQue.front(),[&](Connection* pcon) {unique_lock lock(_queueMutex);pcon->refreshAliveTime(); // 刷新一下开始空闲的起始时间_connectionQue.push(pcon);});_connectionQue.pop();_cv.notify_all();return sp;
}// 从配置文件加载配置项
bool ConnectionPool::loadConfigFile()
{FILE* pf = fopen("mysql.ini", "r");if (pf == nullptr){LOG("mysql.ini file is not exit");return false;}// 如果文件存在while (!feof(pf)){char line[1024] = { 0 };fgets(line, 1024, pf);string str = line;// 从0开始找=号int idx = str.find('=',0);if (idx == -1)// 无效配置项{continue;}// 会有回车 \n int endidx = str.find('\n', idx);string key = str.substr(0, idx);string value = str.substr(idx + 1, endidx - idx - 1);/*cout << key << " " << value << endl;*/if (key == "ip"){_ip = value;}else if (key == "port"){_port = atoi(value.c_str());}else if (key == "username"){_username = value;}else if (key == "password"){_password = value;}else if (key == "dbname"){_dbname = value;}else if (key == "initSize"){_initSize = atoi(value.c_str());}else if (key == "maxSize"){_maxSize = atoi(value.c_str());}else if(key == "maxIdleTime"){_maxIdletime = atoi(value.c_str());}else if (key == "ConnectionTimeOut"){_connectionTimeout = atoi(value.c_str());}}return true;
}

性能测试


测试的代码,注意多线程中的Connection需要在一开始先连接,否则后续同时连接是不行的。这是mysql本身的性质决定。

#include"Connection.h"
using namespace std;
#include"ConnectionPool.h"
void SigleWithConnection()
{time_t begin = clock();for (int i = 0; i < 10000; ++i){ConnectionPool* cp = ConnectionPool::getConnectionPool();shared_ptr sp = cp->getConnection();char sql[1024] = { 0 };sprintf(sql, "insert into user(name,age,sex) values('%s','%d','%s')","zhangsan", 20, "male");sp->update(sql);}time_t end = clock();cout << end - begin << endl;
}
void SigleNoConnection()
{time_t begin = clock();for (int i = 0; i < 10000; ++i){Connection conn;char sql[1024] = { 0 };sprintf(sql, "insert into user(name,age,sex) values('%s','%d','%s')","zhangsan", 20, "male");conn.connect("127.0.0.1", 3307, "root", "123456789", "chat");conn.update(sql);}time_t end = clock();cout << end - begin << endl;
}
void MutiNoConnection()
{Connection conn;conn.connect("127.0.0.1", 3307, "root", "123456789", "chat");time_t begin = clock();thread t1([&]() {for (int i = 0; i < 2500; ++i){Connection conn;char sql[1024] = { 0 };sprintf(sql, "insert into user(name,age,sex) values('%s','%d','%s')","zhangsan", 20, "male");conn.connect("127.0.0.1", 3307, "root", "123456789", "chat");conn.update(sql);}});thread t2([&]() {for (int i = 0; i < 2500; ++i){Connection conn;char sql[1024] = { 0 };sprintf(sql, "insert into user(name,age,sex) values('%s','%d','%s')","zhangsan", 20, "male");conn.connect("127.0.0.1", 3307, "root", "123456789", "chat");conn.update(sql);}});thread t3([&]() {for (int i = 0; i < 2500; ++i){Connection conn;char sql[1024] = { 0 };sprintf(sql, "insert into user(name,age,sex) values('%s','%d','%s')","zhangsan", 20, "male");conn.connect("127.0.0.1", 3307, "root", "123456789", "chat");conn.update(sql);}});thread t4([&]() {for (int i = 0; i < 2500; ++i){Connection conn;char sql[1024] = { 0 };sprintf(sql, "insert into user(name,age,sex) values('%s','%d','%s')","zhangsan", 20, "male");conn.connect("127.0.0.1", 3307, "root", "123456789", "chat");conn.update(sql);}});t1.join();t2.join();t3.join();t4.join();time_t end = clock();cout << end - begin << endl;
}void MutiWithConnection()
{time_t begin = clock();thread t1([]() {for (int i = 0; i < 2500; ++i){ConnectionPool* cp = ConnectionPool::getConnectionPool();shared_ptr sp = cp->getConnection();char sql[1024] = { 0 };sprintf(sql, "insert into user(name,age,sex) values('%s','%d','%s')","zhangsan", 20, "male");if (sp == nullptr){cout << "sp is empty" << endl;continue;}sp->update(sql);}});thread t2([]() {for (int i = 0; i < 2500; ++i){ConnectionPool* cp = ConnectionPool::getConnectionPool();shared_ptr sp = cp->getConnection();char sql[1024] = { 0 };sprintf(sql, "insert into user(name,age,sex) values('%s','%d','%s')","zhangsan", 20, "male");if (sp == nullptr){cout << "sp is empty" << endl;continue;}sp->update(sql);}});thread t3([]() {for (int i = 0; i < 2500; ++i){ConnectionPool* cp = ConnectionPool::getConnectionPool();shared_ptr sp = cp->getConnection();char sql[1024] = { 0 };sprintf(sql, "insert into user(name,age,sex) values('%s','%d','%s')","zhangsan", 20, "male");if (sp == nullptr){cout << "sp is empty" << endl;continue;}sp->update(sql);}});thread t4([]() {for (int i = 0; i < 2500; ++i){ConnectionPool* cp = ConnectionPool::getConnectionPool();shared_ptr sp = cp->getConnection();char sql[1024] = { 0 };sprintf(sql, "insert into user(name,age,sex) values('%s','%d','%s')","zhangsan", 20, "male");if (sp == nullptr){cout << "sp is empty" << endl;continue;}sp->update(sql);}});t1.join();t2.join();t3.join();t4.join();time_t end = clock();cout << end - begin << endl;
}
int main()
{ //MutiNoConnection();MutiWithConnection();//SigleNoConnection();//SigleWithConnection();return 0;
}

测试的条件是本地的虚拟机,mysql也是在本地的。可以看到性能基本上是有一倍的提升的。

数据量未使用连接池花费时间使用连接池花费时间
1000单线程:1273ms 四线程:402ms单线程:606ms 四线程:263ms
5000单线程:7188ms 四线程:1985ms单线程:2923ms 四线程:1258ms
10000单线程:14767ms 四线程:4076ms单线程:5910ms 四线程:2361ms

难点


这个主线程退出的过早,其余线程用到了主线程的部分数据结构,此时使用是会报mutex destory … 的问题的,并且会有productor线程在条件变量下等待,我这边是加多一个bool isRun的字段,在ConnectionPool用最后唤醒线程来解决的。

总结


代码连接码云:https://gitee.com/wuyi-ljh/test-43—testing/tree/master/connectionpool
参考资料:
C++版mysql数据库连接池

相关内容

热门资讯

香港打工数据大揭秘!干啥最赚钱... 来,房姐带你全面了解香港。香港有多少打工人?平均收入多少?哪个行业最赚钱?一个香港家庭要工作多少年才...
深夜重磅!全球首富换人,马斯克... 就因为他手上,还有4550亿美元的订单!一夜之间,全球首富就就变成他了。就在刚刚,全球第二大软件公司...
2025年中报巨亏4000余万... 导读:一边厢,老娘舅的上市步履迈进得越来越迟缓,另一边厢,和其同样在争夺着“中式快餐第一股”头衔的最...
牛市挣的钱,咋留住... 牛市... 图:gothic_jang 牛市之后,赚钱的人多了,大伙问得最多的问题就是: 什么时候卖。 既怕自己...
今夜暴涨,世界首富换人? 今晚... 原创 刘晓博资本市场,每天都在创造奇迹!北京时间9月10日晚上,从美国传来了劲爆的消息:老牌科技公司...
规模不增反降,博时基金酝酿人事... 出品|达摩财经公募基金“老五家”之一的博时基金正酝酿重大人事变动。9月4日,招商局集团发布了下属二级...
阿里宣布高德参战!美团大众点评... 出品|达摩财经成立26年之际,阿里巴巴再度加码本地生活业务。9月10日,阿里巴巴(9988.HK)旗...
单日最大财富增长纪录诞生!埃里... 9月10日,甲骨文公司股价开盘后涨幅迅速扩大至35%,推动其联合创始人拉里·埃里森(Larry El...
半年亏掉6.5亿元, “亲儿子... 过往几年在土地市场激进抢地的“黑马”房企陷入两头“失血”的困境。近期,建发股份披露半年度业绩报告。报...
甲骨文股价暴涨超40%!创始人... 9月10日美股开盘后,甲骨文股价跳空高开,截至北京时间22:06,涨幅飙升至41%,创1992年以来...