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的部分,进行数据库增删查改接口的封装。
连接池模块模块
这个模块是一个单例对象ConnectionPool,负责封装一个个Connection。
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的总量
#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实际上就是对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);
}
#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;// 判断是否还在运行
};
#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数据库连接池