【C++实现】 数据库连接池


涉及知识


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<Connection*> _connectionQue; // 存储mysql链接的队列
mutex _queueMutex; // 维护连接队列线程安全的互斥锁
condition_variable _cv; // 队列条件变量
atomic_int _connectionCnt; // 记录连接所创建的connection的总量

代码剖析

Connection.h


#define _CRT_SECURE_NO_WARNINGS
#pragma once
#include<iostream>
using std::string;
#include<mysql.h>
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、update
	bool update(string sql);

	// 查询操作 select
	MYSQL_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<iostream>

	// 初始化数据库连接
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<string>
#include<queue>
using namespace std;
#include"Connection.h"
#include<mutex>
#include<thread>
#include<functional>
#include<atomic>
#include<condition_variable>
/*
实现连接池模块
*/

class ConnectionPool
{
public:
	static ConnectionPool* getConnectionPool();
	// 给外部提供接口,提供一个可用的空闲连接
	shared_ptr<Connection> getConnection();
	~ConnectionPool();
private:
	void operator=(const ConnectionPool&) = delete;
	ConnectionPool(const ConnectionPool&) = delete;
	ConnectionPool(); // 单例 构造函数私有化
	
	// 运行在独立的线程中,专门负责生产新连接
	void produceConnectionTask();

	// 扫描多余的空闲连接,超过maxIndleTime
	void 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<Connection*> _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();
	 启动一个新的定时线程,扫描多余的空闲连接,超过maxIndleTime
	thread 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<mutex> 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<mutex> lock(_queueMutex); // 由于wait要释放锁,所以用unique_lock
		while (!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<Connection> ConnectionPool::getConnection()
{
	unique_lock<mutex> 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<Connection> sp(_connectionQue.front(),[&](Connection* pcon) {
		unique_lock<mutex> 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<Connection> 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<Connection> 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<Connection> 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<Connection> 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<Connection> 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数据库连接池

数据库连接池是一种常见的优化数据库性能的方法,可以提高应用程序对数据库的访问效率和性能。以下是实现数据库连接池的一些步骤: 1. 初始化连接池:在应用程序启动时,创建一定数量的数据库连接,并将其保存到连接池中。 2. 确定连接池大小:根据应用程序的需求和数据库的负载情况,确定连接池的大小,即可以容纳的最大连接数。 3. 确定连接的有效性:在连接池中的连接可能会因为网络故障或数据库错误而失效。因此,需要对连接进行有效性检查,以确保连接池中只有有效的连接。 4. 管理连接的释放:在应用程序使用完连接后,需要将连接释放回连接池中,以便其他应用程序可以使用它们。同时,需要确保连接在使用后被及时关闭,以避免因连接泄漏而导致的资源浪费。 5. 处理连接池溢出:当连接池中的连接数达到最大值时,需要处理连接池溢出的情况,例如等待其他应用程序释放连接,或者创建新的连接以容纳更多的请求。 6. 定期检查连接池:需要定期检查连接池中的连接是否有效,并清除过期的连接,以避免资源浪费。 7. 实现线程安全:连接池需要实现线程安全,以避免并发访问时出现的问题,例如连接池的竞争条件和死锁问题。可以使用锁或同步机制来实现线程安全。 总之,实现数据库连接池需要考虑很多因素,包括连接的有效性、连接池大小、连接池溢出、释放连接等,需要根据具体的应用程序需求来进行配置和优化。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

^jhao^

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值