基于 C++ 的数据库连接池设计与实现详解

基于 C++ 的数据库连接池设计与实现详解

简介: 数据库连接池(DatebaseConnectionPool)是一种管理数据库连接的技术,通过维护一定数量预先创建的数据库连接,在需要的时候提供给程序使用。以提高系统的性能和资源利用率。当程序需要使用数据库时,从连接池中拿出一个连接 ,用完再将连接返还给连接池。

数据库连接池的核心组件:

最大连接数: 连接池中同时创建的最大数据库连接数。由应用程序的并发要求决定。

最小连接数:连接池再任何时间点维持的最小连接数。

超时时间: 如果没有可用连接,程序等待的时间超过这个值,就做异常处理。

最大空闲时间:如果一个连接从创建一直处于空闲状态,超过这个时间,我们需要将此连接断开。

连接队列:用来把存放创建的连接。

互斥锁、条件变量: 数据库连接池都是用于多线程环境。

连接数据库的必要条件:ip地址、端口号、用户名、密码,数据库名。

满足生产者和消费者模式。

开发环境

Mysql8.0 + vs2022 + Json

为了更贴近实战,我使用了Json来解析数据库连接池的配置。

Json 下载和编译

Jsoncpp是个跨平台的C++开源库,提供的类为我们提供了很便捷的操作,而且使用的人也很多。在使用之前我们首先要从github仓库下载源码,地址如下:
链接:

jsoncpp下载地址

下载cmake工具

通过Cmake工具将从github下载的源码jsoncpp生成一个vs项目,这样就可以编译出需要的库文件了。这里大家百度一下,或者去b站看视频。

配置文件定义 dpconf.json 里面是一个json对象

定义了连接数据库的必要参数 、以及数据库连接池的必要属性。通过读取配置文件设置连接池属性的初始值。

{
	"ip":"127.0.0.1",
	"port":3306,
	"userName":"root",
	"password":"ln123456789",
	"dbName":"school",
	"minSize":100,
	"maxSize":1024,
	"maxDleTime":5000,
	"timeout":1000
}
头文件定义( ConnectionPool.h ) 连接池的整体框架.
#pragma once

#include <queue>
#include <mutex>
#include <condition_variable>
#include <json/json.h>

#include "MysqlConn.h"

using namespace Json;

class ConnectionPool
{
public:
	static ConnectionPool* getInstance( ); // 获取连接池的单例
	
    /*
    	这里返回值是一个智能指针 , 我们通过修改自定义的默认删除器,
    	实现连接的返还操作,这样就不用再次定义一个函数来实现连接的返还。
    */
	std::shared_ptr<MysqlConn>  getMysqlConn(); // 从数据库连接池获取连接

	ConnectionPool(const ConnectionPool& other) = delete;
	ConnectionPool& operator = (const ConnectionPool & other ) = delete;
	
	~ConnectionPool()

protected:
	ConnectionPool( );// 构造函数
private:
	bool parseJsonFile();// 解析配置

	void produce(); // 连接不够,用来增加连接
	void recycle(); // 删除达到最大空闲时间的连接
	void addConnection();// 增加连接数

	std::string  m_ip;   // 数据库的ip地址
	std::string  m_userName;  // 用户名
	std::string  m_passwd;  // 密码
	std::string  m_db;  // 数据库名
	unsigned short m_port; // 端口号

	int m_max_conn; // 最大连接数
	int m_min_conn; // 最小连接数

	int m_timeout;  // 连接超时时间
	int max_del_time; // 最大删除时间( 连接空闲时间超过这个,就给当前连接关闭 )
	std::queue<MysqlConn*>m_connkQueue ;// 连接队列
	std::mutex m_mutex;// 互斥锁
	std::condition_variable m_cond;// 条件变量
};

ConnectionPool.cpp的实现
#include "ConnectionPool.h"

#include <fstream>
#include <iostream>
#include <thread>

ConnectionPool* ConnectionPool::getInstance() {
	static ConnectionPool connPool;
	return &connPool;
}

ConnectionPool::ConnectionPool() {
	// 加载配置文件
	if (!parseJsonFile()) {
		std::cout << "parseJsonFile is failed!" << std::endl;
		return;
	}
	for ( int i = 0 ; i < m_min_conn ; i++ ) {
		addConnection( );
	}
	// 创建一个线程,对连接数进行监控 ,连接数不足就再继续创建
	std::thread producer(&ConnectionPool::produce, this);
	// 对连接数进行监控 ,如果有太多空闲得线程 ,那么就对其进行销毁
	std::thread recycler( &ConnectionPool::recycle,  this);

	// 线程分离
	producer.detach();
	recycler.detach();
}

bool ConnectionPool::parseJsonFile() {

	std::ifstream ifs("dbconf.json");
	Reader rd;
	Value root;
	rd.parse(ifs, root);
	if ( root.isObject( ) ) {
		m_ip = root["ip"].asString();
		m_port = root["port"].asInt();
		m_userName = root["userName"].asString();
		m_passwd = root["password"].asString();
		m_db = root["dbName"].asString();
		m_min_conn = root["minSize"].asInt();
		m_max_conn = root["maxSize"].asInt();
		max_del_time = root["maxDleTime"].asInt();
		m_timeout = root["timeout"].asInt();
		return true;
	}
	return false;
}

ConnectionPool::~ConnectionPool() {
	while (!m_connkQueue.empty()) {
		MysqlConn* conn = m_connkQueue.front();
		m_connkQueue.pop();
		delete  conn;
	}
}

void ConnectionPool::addConnection() {
	MysqlConn* conn = new MysqlConn();
	if ( !conn->connect( m_ip, m_userName, m_passwd, m_db, m_port  ) ) {
		std::cout << "ConnectionPool connect to mysql is failed!" << std::endl;
		delete conn;
		return;
	}
	conn->refreshActiveTime( );
	m_connkQueue.push(conn);
	m_cond.notify_one();   // 唤醒一个线程
}

void ConnectionPool::produce(){
	while (true) {
		std::unique_lock<std::mutex> lock(m_mutex);
        // 连接队列的数量小于的连接数则说明连接池中的连接不够,需要增加连接
		while (m_connkQueue.size() >= m_min_conn) { 
			m_cond.wait(lock);
		}
		addConnection();
	}
}

// 删除空闲连接
void ConnectionPool::recycle() {
	while ( true ) {
		std::this_thread::sleep_for( std::chrono::milliseconds(500));// 休眠500 ms
		std::unique_lock<std::mutex>lock(m_mutex);
		while ( m_connkQueue.size() > m_min_conn ) {
			MysqlConn* conn = m_connkQueue.front( );
			if (conn->getActiveTime() >= max_del_time) {
				m_connkQueue.pop();
				delete conn;
			}
			else {
				break;
			}
		}
	}
}

std::shared_ptr<MysqlConn>  ConnectionPool::getMysqlConn() {
	
	std::unique_lock<std::mutex>lock(m_mutex);
	while ( m_connkQueue.empty()) {
		// 如果等待一段时间后,队列还是为空,返回一个 null
		if (std::cv_status::timeout == m_cond.wait_for(lock, std::chrono::milliseconds( m_timeout ))) {
			if( m_connkQueue.empty( ) )    return nullptr;
		}
	}
    // 指定智能指针的删除器 ,实现连接返还给连接池( 再次加入到连接队列中 )
	std::shared_ptr<MysqlConn> connPtr(std::move( m_connkQueue.front()) , [this](MysqlConn* conn) {
		std::unique_lock <std::mutex>lock(m_mutex) ;
		conn->refreshActiveTime();
		m_connkQueue.push(conn);
		});
	m_connkQueue.pop();
	m_cond.notify_one();// 唤醒阻塞的生产者线程,开始生产
	return connPtr;
}

数据库连接池是需要连接数据库的.

将连接数据库封装成类 ,提供对外操作的方法.

封装数据库连接类 MysqlConn.h 头文件的实现
 #pragma once
 
 #include <string>
 #include <mysql.h>
 #include <string.h>
 #include <chrono>
 
 class MysqlConn
 {
 public:
 	// 初始化连接
 	MysqlConn();
 	// 连接数据库
 	bool connect(std::string ip, std::string userName, std::string passwd, std::string db , int port = 3306);
 	// 释放资源
 	~MysqlConn( );
 	// 更新数据库  ( insert  update delete )
 	bool update(std::string sql);
 	// 查询数据库
 	bool query(std::string sql);
 	// 得到结果集
 	bool getResult();
 	// 获取结果集的字段
 	std::string getField(int index);
 	// 事务操作
 	bool transaction();
 	// 提交事务
 	bool commit( );
 	// 事务回滚
 	bool rollback();
 	/*
 		用来辅助数据库的连接后的空闲时间
 	*/
 	void refreshActiveTime( );   // 刷新活跃时间
 	long long getActiveTime();   // 获取当前活跃的时间间隔 
 
 private:
 
 	void freeRes();   // 释放结果集
 	MYSQL* mysql_ = NULL ; // 数据库连接句柄
 	MYSQL_RES* res_ = NULL;// 指向返回的结果集指针,查询到的所有结果(多行)
 	MYSQL_ROW row_ = nullptr;// 指向结果集中具体的一行
 	std::chrono::steady_clock::time_point  activeTime_; //稳定时间测量 
 };
MysqlConn.cpp文件的实现
#include "MysqlConn.h"

// 初始化连接
MysqlConn::MysqlConn() {
	mysql_ = mysql_init( mysql_ );
	// 设置字符集
	if( mysql_ )   mysql_set_character_set(mysql_ , "gbk");
}

// 连接数据库
bool MysqlConn::connect( std::string ip, std::string userName, std::string passwd, std::string db, int port ) {
	mysql_ = mysql_real_connect(mysql_, ip.c_str(), userName.c_str(), passwd.c_str(), db.c_str(), port, nullptr, 0);
	if (!mysql_) {
		return false;
	}
	return true;
}

// 释放资源
MysqlConn::~MysqlConn() {
	if (mysql_) {
		mysql_close(mysql_);
		mysql_ = nullptr;
	}
	freeRes();
}
// 更新数据
bool MysqlConn::update( std::string sql ) {
	int ret = mysql_query( mysql_ , sql.c_str());
	if( ret != 0 ){
		return false;
	}
	return true;
}

// 查询数据库
bool MysqlConn::query(std::string sql) {
	freeRes( );
	int ret = mysql_query(mysql_, sql.c_str());
	if (ret != 0)   return false;
	
	// 获取查询结果
	res_ = mysql_store_result(mysql_);
	if (!res_)   return false;
	return true;
}

// 得到结果集
bool MysqlConn::getResult() {
	if (res_) {
		row_ = mysql_fetch_row(res_);
		if(row_)  return true;
	}
	return false;
}

// 获取结果集的字段
std::string MysqlConn::getField( int index ) {
	int cols = mysql_num_fields(res_);
	if ( index >= cols || index < 0 )   return std::string("");
	
	char* value = row_[index];
	unsigned long  len = mysql_fetch_lengths(res_)[index];
	return  std::string(value, len);
}

// 事务操作
bool MysqlConn::transaction() {
	return mysql_autocommit(mysql_ ,false );
}

// 提交事务
bool MysqlConn::commit() {
	return mysql_commit(mysql_);
}

// 事务回滚
bool MysqlConn::rollback() {
	return mysql_rollback(mysql_);
}

void MysqlConn::refreshActiveTime()
{
	activeTime_  = std::chrono::steady_clock::now();
}

long long MysqlConn::getActiveTime()
{
	 // 纳米
	 std::chrono::nanoseconds  nased =  std::chrono::steady_clock::now() - activeTime_;
	 // 转换成毫米
	 std::chrono::microseconds millsed = std::chrono::duration_cast<std::chrono::microseconds>( nased );
	 return millsed.count( );   //多少毫秒
}

void MysqlConn::freeRes() {
	if (res_) {
		mysql_free_result(res_);
		res_ = nullptr;
	}
}
测试结果分析

测试条件,每次操作之前都将数据库进行清空(因为我测试的表有一个主键,主键是唯一的 )

delete tableName from databaseName;

单线程环境下 ,使用数据库连接池和不使用数据库连接池。创建5000次连接,每次连接都向数据库中插入一条数据。

多线程环境下,使用数据库连接池和不使用数据库连接池,创建5000次连接,每次连接都向数据库中插入一条数据。

代码如下:

#include <iostream>

#include <queue>
#include <vector>
#include "ConnectionPool.h"

void func(const char* str) {
	std::cout << str << std::endl;
}

// 测试数据库增删查改功能
int query() {

	MysqlConn* conn = new MysqlConn();
	conn->connect("127.0.0.1", "root", "ln123456789", "school", 3306);
	std::string insert_sql( "insert into student values('002' ,'猪八戒' ,20 )" );
	conn->update( insert_sql );
	std::string insert_sql1( "insert into student values('003' ,'沙和尚' ,20 )" );
	conn->update(insert_sql1);
	std::string update_sql("update student set age = 100 where id ='002'");
	conn->update(update_sql);
	std::string delete_sql("delete from  student where name='沙和尚'");
	conn->update(delete_sql);
	// 查询
	conn->query("select *from student");
	while( conn->getResult() ){
		// 获取字段
		std::cout << "id: " << conn->getField(0)
			      << "  name: " << conn->getField(1)
			      << " age:" << conn->getField(2)<<std::endl;
	}
	return 1;
}

// opt : 单线程不使用数据库连接池
// opt1: 单线程使用数据库连接池
// 创建五千次连接 ,并且每次插入一条数据( 数据库为空时 )
void opt(int begin, int end) {   
	for (int i = begin; i < end; i++) {
		MysqlConn conn;
		conn.connect("127.0.0.1", "root", "ln123456789", "school", 3306);
		char sql[1024] = { 0 };
		sprintf(sql, "insert into student values('%d' , '吴献丰' ,22)", i + 1);
		conn.update(sql);
	}
}

void opt1(int begin, int end, ConnectionPool* cpool) {
	cpool->getInstance( );
	for (int i = begin; i < end; i++) {
		std::shared_ptr<MysqlConn>conn = cpool->getMysqlConn();
		char sql[1024] = { 0 };
		sprintf(sql, "insert into student values('%d' , '吴献丰' ,22)", i + 1);
		conn->update(sql);
	}
}

void singleThreadTest( ) {
#if 0
	// 单线程不使用连接池耗时 :28070452400毫秒: 28070
	std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
	opt(1, 5000);
	std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
	auto spendTime = end - start;
	std::cout << "单线程不使用连接池耗时 :" << spendTime.count() <<
		"毫秒: " << spendTime.count() / 1000000 << std::endl;
#else
	// 单线程使用连接池耗时 :12442017600  毫秒: 12442
	std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
	ConnectionPool* cpool = ConnectionPool::getInstance();
	opt1(1, 5000, cpool);
	std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
	auto spendTime = end - start;
	std::cout << "单线程使用连接池耗时 :" << spendTime.count() <<
		"  毫秒: " << spendTime.count() / 1000000 << std::endl;
#endif
}

void threadsTest() {
#if 1
	// 多线程不使用连接池耗时 :10546208100毫秒: 10546
	std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
	MysqlConn conn; ( 防止同一时间多个连接使用一个用户名去连接数据库导致失败的问题 )
	conn.connect("127.0.0.1", "root", "ln123456789", "school", 3306);
	std::thread task1( opt, 0, 1000 );
	std::thread task2(opt, 1000, 2000 );
	std::thread task3(opt, 2000, 3000 ) ;
	std::thread task4(opt, 3000, 4000 );
	std::thread task5(opt, 4000, 5000 );
	task1.join();
	task2.join();
	task3.join();
	task4.join();
	task5.join();
	std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
	auto spendTime = end - start;
	std::cout << "多线程不使用连接池耗时 :" << spendTime.count() <<
		"毫秒: " << spendTime.count() / 1000000 << std::endl;
#else
	// 多线程使用连接池耗时 :4861039100  毫秒: 4861
	std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
	ConnectionPool* cpool = ConnectionPool::getInstance();
	std::thread task1( opt1, 0, 1000,cpool);
	std::thread task2( opt1, 1000, 2000, cpool);
	std::thread task3( opt1, 2000, 3000, cpool);
	std::thread task4( opt1, 3000, 4000, cpool);
	std::thread task5( opt1, 4000, 5000, cpool);
	task1.join();
	task2.join();
	task3.join();
	task4.join();
	task5.join();
	std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
	auto spendTime = end - start;
	std::cout << "多线程使用连接池耗时 :" << spendTime.count() <<
		"  毫秒: " << spendTime.count() / 1000000 << std::endl;
#endif
}

int main( ) {
	
	threadsTest();

	return 0;
}

结论: 在程序需要频繁对数据库进行访问的情况下,使用数据库连接池可以大大提高程序的性能。
抄作业地址:gitee

  • 17
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

零二年的冬

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

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

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

打赏作者

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

抵扣说明:

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

余额充值