C++ mySQL数据库连接池(windows平台)

C++ MySQL数据库连接池

新手学了C++多线程,看了些资料练手写了C++数据库连接池小项目,自己的源码地址

关键技术点

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

连接池项目

为了提高MySQL数据库(基于C/S设计)的访问瓶颈,除了在服务器端增加缓存服务器缓存常用的数据之外(例如redis),还可以增加连接池,来提高MySQL Server的访问效率,在高并发情况下,大量的TCP三次握手、MySQL Server连接认证、MySQL Server关闭连接回收资源和TCP四次挥手所耗费的性能时间也是很明显的,增加连接池就是为了减少这一部分的性能损耗。
在市场上比较流行的连接池包括阿里的druid,c3p0以及apache dbcp连接池,它们对于短时间内大量的数据库增删改查操作性能的提升是很明显的,但是它们有一个共同点就是,全部由Java实现的。
那么本项目就是为了在C/C++项目中,提供MySQL Server的访问效率,实现基于C++代码的数据库连接池模块。

连接池功能点介绍

连接池一般包含了数据库连接所用的ip地址、port端口号、用户名和密码以及其它的性能参数,例如初始连接量,最大连接量,最大空闲时间、连接超时时间等,该项目是基于C++语言实现的连接池,主要也是实现以上几个所有连接池都支持的通用基础功能。

  • 初始连接量(initSize):表示连接池事先会和MySQL Server创建initSize个数的connection连接,当应用发起MySQL访问时,不用再创建和MySQL Server新的连接,直接从连接池中获取一个可用的连接就可以,使用完成后,并不去释放connection,而是把当前connection再归还到连接池当中。

  • 最大连接量(maxSize):当并发访问MySQL Server的请求增多时,初始连接量已经不够使用了,此时会根据新的请求数量去创建更多的连接给应用去使用,但是新创建的连接数量上限是maxSize,不能无限制的创建连接,因为每一个连接都会占用一个socket资源,一般连接池和服务器程序是部署在一台主机上的,如果连接池占用过多的socket资源,那么服务器就不能接收太多的客户端请求了。当这些连接使用完成后,再次归还到连接池当中来维护。

  • 最大空闲时间(maxIdleTime):当访问MySQL的并发请求多了以后,连接池里面的连接数量会动态增加,上限是maxSize个,当这些连接用完再次归还到连接池当中。如果在指定的maxIdleTime里面,这些新增加的连接都没有被再次使用过,那么新增加的这些连接资源就要被回收掉,只需要保持初始连接量initSize个连接就可以了。

  • 连接超时时间(connectionTimeout):当MySQL的并发请求量过大,连接池中的连接数量已经到达maxSize了,而此时没有空闲的连接可供使用,那么此时应用从连接池获取连接无法成功,它通过阻塞的方式获取连接的时间如果超connectionTimeout时间,那么获取连接失败,无法访问数据库。

实现的逻辑图片

数据表的结构

文章内容不会将MySQL的安装,基于你已经下载了mysql server 8.0 ,我们建立一个mysql数据库的数据表来演示后面如何用C++连接数据表,并且写SQL.

先进入mysql,输入密码

mysql -u root -p

创建一个数据库名叫chat,同时创建数据表

CREATE DATABASE chat;
use chat;
CREATE TABLE user (
       id INT(11) NOT NULL AUTO_INCREMENT,
      name VARCHAR(50) NOT NULL,
      age INT(11) NOT NULL,
        sex ENUM('male', 'female') NOT NULL,
        PRIMARY KEY (id)
      );

如果输出OK就代表创建user好了,我们来看看数据表

desc user;
+-------+-----------------------+------+-----+---------+----------------+
| Field | Type                  | Null | Key | Default | Extra          |
+-------+-----------------------+------+-----+---------+----------------+
| id    | int                   | NO   | PRI | NULL    | auto_increment |
| name  | varchar(50)           | NO   |     | NULL    |                |
| age   | int                   | NO   |     | NULL    |                |
| sex   | enum('male','female') | NO   |     | NULL    |                |
+-------+-----------------------+------+-----+---------+----------------+

查看一下内容,没有

mysql> select * from user;
Empty set (0.19 sec)

到这里我们MySQL表就创建好了,我们不用管他,我们进行编写CPP连接数据库代码

连接数据库,并且执行sql语句

打开VS2019,并且创建一个控制台项目,项目结构如图

main.cpp负责执行主函数代码,Connect负责编写封装数据库的连接和sql操作,mysqlPool负责编写数据库连接池。

但我们还不急着编写代码,先导入需要的外部库,在VS上需要进行相应的头文件和库文件的配置,如下:

  • 1.右键项目 - C/C++ - 常规 - 附加包含目录,填写mysql.h头文件的路径
  • 2.右键项目 - 链接器 - 常规 - 附加库目录,填写libmysql.lib的路径
  • 3.右键项目 - 链接器 - 输入 - 附加依赖项,填写libmysql.lib库的名字
  • 4.把libmysql.dll动态链接库(Linux下后缀名是.so库)放在工程目录下

如果你没有修改过MySQL路径,一般mysql.h在你的电脑路径如下

如果你没有修改过MySQL路径,一般libmysql.lib在你的电脑路径如下

libmysql.dll文件存放在你项目文件路径下面

1.封装Mysql.h的接口成connection类

接下来封装一下mysql的数据库连接代码,不懂的看看注释,也很简单的调用Mysql.h的接口,我们在connection中额外加入创建时间函数和存活时间函数,不能让空闲的线程存活时间超过定义的最大空闲时间

connect.h的代码如下

#pragma once
#include <mysql.h>
#include <string>
#include <ctime>
using namespace std;
/*
封装MySQL数据库的接口操作
*/
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 getAliveeTime()const { return clock() - _alivetime; }
private:
	MYSQL* _conn; // 表示和MySQL Server的一条连接
	clock_t _alivetime; // 记录进入空闲状态后的起始存活时间
};

connect.cpp的代码如下

 
#include "public.h"
#include "Connect.h"
#include <iostream>
using namespace std;

Connection::Connection()
{
	// 初始化数据库连接
	_conn = mysql_init(nullptr);
}

Connection::~Connection()
{
	// 释放数据库连接资源
	if (_conn != nullptr)
		mysql_close(_conn);
}

bool Connection::connect(string ip, unsigned short port,
	string username, string password, string dbname)
{
	// 连接数据库
	MYSQL* p = mysql_real_connect(_conn, ip.c_str(), username.c_str(),
		password.c_str(), dbname.c_str(), port, nullptr, 0);
	return p != nullptr;
}

bool Connection::update(string sql)
{
	// 更新操作 insert、delete、update
	if (mysql_query(_conn, sql.c_str()))
	{
		LOG(+ "更新失败:" + sql);
		return false;
	}
	return true;
}

MYSQL_RES* Connection::query(string sql)
{
	// 查询操作 select
	if (mysql_query(_conn, sql.c_str()))
	{
		LOG("查询失败:" + sql);
		return nullptr;
	}
	return mysql_use_result(_conn);
}

在public.h中编写的代码,帮助我们输出日志和警告

#pragma once
#include <iostream>
#define LOG(str) \
	std::cout << __FILE__ << ":"<<__LINE__<<" " \
	__TIMESTAMP__ << ":"<<str <<std::endl;

使用这个宏,你可以在代码中的任何地方轻松输出日志信息。

我们暂时用这个main先测试下connection类

main.cpp代码

#include <iostream>
#include "Connect.h"
int main()
{
	Connection conn;
	char sql[1024] = { 0 };
//插入一条数据
	sprintf(sql, "insert into user(name,age,sex) values('%s','%d','%s');", "zhang san", 20, "male");
	conn.connect("127.0.0.1", 3306, "root", "123456", "chat");
	conn.update(sql); //更新sql语句
	return 0;
}

如果你的vs2019给你报安全警告,应该是sprintf的问题,你右击项目,选择属性中C++的常规中SDL检查,设置为否。
编译运行后,我们返回MySQL的界面,发现数据已经插入成功了

mysql> select * from user;
+----+-----------+-----+------+
| id | name      | age | sex  |
+----+-----------+-----+------+
|  1 | zhang san |  20 | male |
+----+-----------+-----+------+
1 row in set (0.02 sec)

现在我们已经成功能调用外部接口来连接Mysql数据库了,接下来我们来编写连接池.

2.编写连接池

2.1MySQL配置文件和加载配置文件

我们来编写mySqlPool的代码,因为数据库连接池只有一个,所以我们写成单例模式。同时会有多个服务端进入连接池,所以我们要添加互斥锁来避免线程之间的冲突。
我们在项目中创建一个名叫mysql.ini配置文件存储数据库连接的信息,例如数据库ip地址,用户名,密码等
mysql.ini的内容如下,如果你的用户名和密码跟里面不同,请修改

#数据库连接池的配置文件
ip=127.0.0.1
port=3306
username=root
password=123456
initSize=10
maxSize=1024
#最大空闲时间默认单位为秒
maxIdleTime=60
#连接超时时间单位是毫米
connectionTimeOut=100

我们把mysqlPool.h文件中需要的函数都声明好,等会在cpp中实现。

#pragma once
#include "public.h"
#include "Connect.h"
#include <queue>
#include <mutex>
#include <string>
#include <atomic>
#include <memory>
#include <functional>
#include <condition_variable>
//因为数据库连接池子只有一个,所以我们采用单例模式
class mySqlPool {
public:
	//获取连接池对象实例
	static mySqlPool* getMySqlPool();
	std::shared_ptr<Connection> getConnection();//从连接池获取一个可用的空闲连接
private:
	mySqlPool();//构造函数私有化
	bool loadConfigFile();//从配置文件中加载配置项
	void produceConnectionTask(); //运行在独立的线程中,专门负责生产新连接
	//扫描超过maxIdleTime时间的空闲连接,进行队列的连接回收
	void scannerConnectionTask();

	std::string _ip;//mysql的ip地址
	std::string _dbname;//数据库的名称
	unsigned short _port; //mysql端口号3306
	std::string _username;//mysql用户名
	std::string _password;//mysql登陆密码
	int _initSize;//连接池的初始连接量
	int _maxSize;//连接池的最大连接量
	int _maxIdleTime;//连接池最大空闲时间
	int _connectionTimeOut;//连接池获取连接的超时时间

	std::queue<Connection*> _connectionQue;//存储mysql连接队列
	std::mutex _queueMutex; //维护连接队列的线程安全互斥锁
	std::atomic_int _connectionCnt; //记录连接所创建的connect的数量
	std::condition_variable cv;//设置条件变量,用于生产者线程和消费者线程的通信
};

编写mySqlPool.cpp 中加载我们上面.ini配置文件的函数

//在mySqlPool.cpp中
//加载配置文件
bool mySqlPool::loadConfigFile()
{
	FILE* pf = fopen("mysql.ini", "r");
	if (pf == nullptr)
	{
		LOG("mysql.ini file is not exits!");
		return false;
	}
	while (!feof(pf)) //遍历配置文件
	{
		char line[1024] = { 0 };
		fgets(line, 1024, pf);
		std::string str = line;
		int idx = str.find('=', 0); //从0开始找'='符号的位置
		if (idx == -1)continue;
		int endidx = str.find('\n', idx);//从idx寻找'\n'的位置,也就是末尾
		std::string key = str.substr(0, idx); //获取配置文件中=号左边的key
		//从等号后到末尾,刚好是value的string形式
		std::string value = str.substr(idx + 1, endidx - idx - 1);
		if (key == "ip")
		{
			_ip = value;
		}
		else if (key == "port")
		{
			//字符串转换成unsigned short
			_port = static_cast<unsigned short>(std::stoul(value));
		}
		else if (key == "username")
		{
			_username = value;
		}
		else if (key == "password")
		{
			_password = value;
		}
		else if (key == "dbname")
		{
			_dbname = value;
		}
		else if (key == "initSize")
		{
			_initSize = std::stoi(value); 
		}
		else if (key == "maxSize")
		{
			_maxSize = std::stoi(value); 
		}
		else if (key == "maxIdleTime")
		{
			_maxIdleTime = std::stoi(value); 
		}
		else if (key == "connectionTimeOut")
		{
			_connectionTimeOut = std::stoi(value); 
		}
	}
	return true;
}

这样我们加载配置文件就完成了

2.2编写连接池单例模式

单例模式确保数据库连接池在整个应用程序中只有一个实例。这样,所有需要数据库连接的线程或操作都可以从这个池中获取连接,而不是每次都创建新的连接。这大大减少了资源消耗和性能损耗。(如果不懂数据模式单例模式可以百度一下)

我们在.h文件中,我们先将构造函数private化,这样外部就只能通过接口来获取,我们在cpp中来编写具体的实现代码
构造方法

//mySqlPool.h
//构造方法
mySqlPool::mySqlPool()
{
	if (!loadConfigFile())
	{
		LOG("load Config File is error!");
		return;
	}
	//创建初始数量的连接
	for (int i = 0; i < _initSize; ++i)
	{
		Connection* p = new Connection();
		p->connect(_ip, _port, _username, _password, _dbname);
	}
	//启动一个新线程,作为连接的生产者
	std::thread produce(std::bind(&mySqlPool::produceConnectionTask, this));
	produce.detach();
	//启动一个新线程,作为空闲连接超时的回收者
	std::thread scanner(std::bind(&mySqlPool::scannerConnectionTask, this));
	scanner.detach();
}

单例模式

//mySqlPool.h
//单例模式
mySqlPool* mySqlPool::getMySqlPool()
{
	static mySqlPool pool;
	return &pool;
}

现在我们已经成功的编写了单例模式,接下来我们开始获取数据库的连接。

数据库连接的线程通信

我们创建一个connect*线程队列queue来存放MySQL数据库的连接connect,同时我们还会额外创建两个线程。

一个线程是生产者,开始从Connect类中获取initSize个连接加入连接队列中准备着,当判断连接队列empty,又开始获取连接加入连接队列中 ,如果不为empty就进入阻塞状态。

生产者线程代码

//运行在独立的线程中,专门负责生产新连接
void mySqlPool::produceConnectionTask()
{
	while (true)
	{
		std::unique_lock<std::mutex> lock(_queueMutex);
		while (!_connectionQue.empty())
			cv.wait(lock);     //队列不为空不生产线程

		//没有到上线就可以生产线程
		if (_connectionCnt < _maxSize)
		{
			auto p = new Connection();
			p->connect(_ip, _port, _username, _password, _dbname);
			p->refreshAliveTime();//创建的时候刷新存活时间
			_connectionQue.push(p);
			++_connectionCnt;
		}
		cv.notify_all();
	}
}

另外一个线程是消费者,如果服务端想要获取队列中的连接,消费者线程将会从队列中拿出connection来,如果队列为empty,线程会处于阻塞状态。

消费者线程代码

/从连接池获取一个可用的空闲连接
std::shared_ptr<Connection> mySqlPool::getConnection()
{
	std::unique_lock<std::mutex> lock(_queueMutex);
	while (_connectionQue.empty())
	{
		//如果超时没有获取可用的空闲连接返回空
		if (std::cv_status::timeout == cv.wait_for(lock, std::chrono::milliseconds(100)))

			if (_connectionQue.empty())
			{
				LOG("get Connection error");
				return nullptr;
			}
	}
	std::shared_ptr<Connection> sp(_connectionQue.front(), [&](Connection* pcon) {
		//保证只能同一时刻只能有一个线程归还连接给队列
		std::unique_lock<std::mutex> lock(_queueMutex);
		pcon->refreshAliveTime();//创建的时候刷新存活时间
		_connectionQue.push(pcon);
		});
	_connectionQue.pop();
	cv.notify_all();
	return sp;
}

如果队列里面大于初始个数的新connection空闲时间大于最大空闲时间,我们将会回收该连接(但是不会完全释放,我们将其归还在连接池中)。

上面getConnection代码的这段就是实现了回收功能

std::shared_ptr<Connection> sp(_connectionQue.front(), [&](Connection* pcon) {
		//保证只能同一时刻只能有一个线程归还连接给队列
		std::unique_lock<std::mutex> lock(_queueMutex);
		pcon->refreshAliveTime();//创建的时候刷新存活时间
		_connectionQue.push(pcon);
		});

扫描超过maxIdleTime时间的空闲连接,进行队列的连接回收

//连接线程回收
void mySqlPool::scannerConnectionTask()
{
	while (true)
	{
		//通过sleep模拟定时效果,每_maxIdleTime检查一次
		std::this_thread::sleep_for(std::chrono::seconds(_maxIdleTime));
		
		//扫描整个队列释放多余的超时连接
		std::unique_lock<std::mutex> lock(_queueMutex);
		while (_connectionCnt > _initSize)
		{
			auto p = _connectionQue.front();
			if (p->getAliveeTime() >= (_maxIdleTime * 1000))
			{
				_connectionQue.pop();
				delete p;//这里会调用智能指针,回收到队列中
			}
		}
	}
}

到这里,我们连接池的代码已经完成了,接下来是测试一下代码

连接池的压力测试

我们分别测试连接个数为10,100,1000时候的性能差异,创建一个test.h文件,编写测试代码
注意下面的测试可能根据不同的电脑性能,可能速度会有所差异。

普通连接

//test.h
//非线程池的连接
void testSql( int n)
{
	clock_t begin = clock();
	std::thread t([&n]() {
		for (int i = 1; i < n; ++i)
		{
			Connection cnn;
			char sql[1024] = { 0 };
			sprintf(sql, "insert into user(name,age,sex) values('%s',%d,'%s')", "zhang san", 20, "male");
			cnn.connect("127.0.0.1", 3306, "root", "123456", "chat");
			cnn.update(sql);
		}});
	t.join();
	clock_t end = clock();
	std::cout << "普通连接数量为:" << n << "的sql执行时间:" << (end - begin) << "ms" << std::endl;
}

main.cpp中调用

#include <iostream>
#include "Connect.h"
#include "mySqlPool.h"
#include "test.h"

int main()
{
	testSql(10);//普通连接数量为 : 10的sql执行时间 : 2838ms
	testSql(100);//普通连接数量为 : 100的sql执行时间: 12299
	testSql(1000);//普通连接数量为 : 1000的sql执行时间 : 104528ms
	return 0;
}

单线程的线程池

 //test.h
void f(int n)
{
	mySqlPool* cp = mySqlPool::getMySqlPool();
	for (int i = 1; i <= n; ++i)
	{
		std::shared_ptr<Connection> sp = cp->getConnection();
		char sql[1024] = { 0 };
		sprintf(sql, "insert into user(name,age,sex) values('%s',%d,'%s')", "zhang san", 20, "male");
		sp->update(sql);
	}
}
//测试连接池连接
void testSqlPool(int n)
{
	clock_t begin = clock();
	std::thread t1(f, n);
	t1.join();
	clock_t end = clock();
	std::cout << "单线程采用数据库连接池,连接数量为:" << n << "的sql执行时间:" << (end - begin) << "ms" << std::endl;
}

main.cpp中调用

#include <iostream>
#include "Connect.h"
#include "mySqlPool.h"
#include "test.h"

int main()
{
  testSqlPool(10);//单线程 采用数据库连接池,连接数量为:10的sql执行时间:1745ms
  testSqlPool(100);//单线程 采用数据库连接池,连接数量为:100的sql执行时间:9779ms 
  testSqlPool(1000);//单线程 采用数据库连接池,连接数量为:1000的sql执行时间 : 86016ms
	return 0;
}

多线程的线程池

//test.h
//测试连接池连接 4线程
void testSqlPool4(int n)
{
	int n2 = n / 4;
	clock_t begin = clock();
	std::thread t1(f, n2);
	std::thread t2(f, n2);
	std::thread t3(f, n2);
	std::thread t4(f, n2);
	t1.join();
	t2.join();
	t3.join();
	t4.join();
	clock_t end = clock();
	std::cout << "四线程采用数据库连接池,连接数量为:" << n << "的sql执行时间:" << (end - begin) << "ms" << std::endl;
}

main.cpp中调用

#include <iostream>
#include "Connect.h"
#include "mySqlPool.h"
#include "test.h"

int main()
{
	testSqlPool4(100);//4条线程 采用数据库连接池,连接数量为:100的sql执行时间 : 3715ms
    testSqlPool4(1000);//4条线程 采用数据库连接池,连接数量为:1000的sql执行时间 : 34686ms
	return 0;
}

由上面测试数据可以得出,普通连接<单线程连接池<多线程连接池,连接池比普通连接还是优化很多的。
文章里面如果有问题的请评论讲出,希望可以多多包含一下新人不足,如果看完了还是对于代码很陌生,可以下载来看一看。源码地址

热门相关:虎狼之师   最牛兵王   萌妻太甜:总裁大人,别傲娇   神医嫁到   混在三国当军阀