C++实现简易数据库连接池的研究

前言

近期准备秋招,于是将之前的WebServer项目捡起来,重新设计整理了一番。参考了Github上一些高星项目,引入了数据库连接池,来提升服务器业务处理部分的性能。

本文致力于让非科班的入门级程序员能够理解数据库连接池的原理,所以在表述上可能有些不当之处,敬请谅解。

一、数据库连接池的简介

1.1 数据库连接池的定义

数据库连接池是指,在服务器程序未和客户端通讯时,就提前创建好一定数量的数据库连接并放入缓冲池中;当服务器工作线程需要请求建立数据库连接时,就可以直接从缓冲池中“拿出”建立好的连接来用,用完后将该连接放回池中(动态绑定机制),连接本身不会中断。

数据库连接池负责分配,管理和释放数据库连接,它允许服务器程序重复使用一个现有的数据库连接,而不是重新建立一个。(即连接池中的连接是公共的,谁都能用,你用完我可以接着用)

当线程向连接池请求的连接数超过最大连接数量时,这些请求将被加入到等待队列中。

1.2 为什么要建立数据库连接池

1、减少连接的创建和销毁开销
数据库连接的创建和销毁是相对昂贵的操作。连接池通过在应用程序启动时创建一组连接,并在需要时将其分配给应用程序,从而减少了每次请求都创建和销毁连接的开销。

2、避免连接泄漏:
连接池监控连接的状态,可以在连接空闲时间过长或者发生异常时进行回收。这有助于防止连接泄漏,即长时间占用的连接没有被及时释放。

3、限制并发连接数
连接池可以限制同时存在的连接数量,防止过多的连接占用数据库资源。这通过连接池的最大连接数和最大溢出连接数来控制,确保系统不会超过容量。

4、连接的超时处理
连接池可以设置连接的超时时间,即连接在一定时间内没有被使用就会被回收。这有助于释放不再需要的连接,避免长时间占用连接的问题。

5、提高应用程序的可扩展性:
通过减少数据库连接的创建和销毁开销,连接池有助于提高应用程序的可伸缩性。在高负载时,系统可以更好地处理更多的请求。

1.3 数据库连接的实现

1、首先要包含mysql的头文件,并链接mysql动态库。

#include <mysql.h>

2、创建MYSQL句柄。

MYSQL mysql;

3、mysql_init初始化MYSQL句柄。

MYSQL *mysql_init(MYSQL *mysql) 

4、调用mysql_real_connect函数连接Mysql数据库。

MYSQL *mysql_real_connect(MYSQL *mysql, const char *host, const char *user, const char *passwd, const char *db, unsigned int port, const char *unix_socket, unsigned long client_flag) 

mysql:前面定义的MYSQL句柄;

host:MYSQL服务器的地址;如果“host”是NULL或字符串"localhost",连接将被视为与本地主机的连接。如果操作系统支持套接字(Unix)或命名管道(Windows),将使用它们而不是TCP/IP连接到服务器。

user:登录用户名;如果“user”是NULL或空字符串"",用户将被视为当前用户。在UNIX环境下,它是当前的登录名。

passwd:登录密码;

db:要连接的数据库,如果db为NULL,连接会将默认的数据库设为该值。

port:MYSQL服务器的TCP服务端口,一般为3306;如果 port 不是0,其值将用作 TCP/IP 连接的端口号。注意,host 参数决定了连接的类型。

unix_socket:unix 连接方式。为 NULL 则是 TCP/IP 连接到服务器。如果 unix_socket 不是NULL,该字符串描述了应使用的套接字或命名管道。注意,host 参数决定了连接的类型。

clientflag:Mysql 运行为 ODBC 数据库的标记,一般取0。

5、调用mysql_real_query函数进行数据库查询。

int mysql_query(MYSQL *mysql, const char *query) 
//query为执行的SQL语句对应的字符长串

6、通过调用mysql_store_result或mysql_use_result函数返回的MYSQL_RES变量获取查询结果数据。

int   mysql_real_query(MYSQL *mysql, const char *q, unsigned long length);

7、调用mysql_fetch_row函数读取结果集数据。

MYSQL_ROW mysql_fetch_row(MYSQL_RES* result)
//MYSQL_ROW为char ** 类型

8、结果集用完后,调用mysql_free_result函数释放结果集,以防内存泄露。

void mysql_free_result(MYSQL_RES *result) 

9、不再查询Mysql数据库时,调用mysql_close函数关闭数据库连接。

void mysql_close(MYSQL *mysql) 

二、数据库连接池的代码实现

2.1 数据库<mysql/mysql.h>的使用

同上1.3,具体的涉及到数据库的操作,我将在C++如何解析HTTP报文,实现HTTP协议的文章中详细描述。

2.2 POSIX线程库<pthread.h> 的使用

参考日志系统中对线程库的使用说明:

C++实现简易异步LOG日志系统的研究-CSDN博客

2.3 数据库连接池的设计模式

#include <mysql/mysql.h>
#include <stdio.h>
#include <string>
#include <string.h>
#include <stdlib.h>
#include <list>
#include <pthread.h>
#include <iostream>
#include "sql_connection_pool.h"

using namespace std;

connection_pool::connection_pool()
{
	m_CurConn = 0;
	m_FreeConn = 0;
}

connection_pool *connection_pool::GetInstance()
{
	static connection_pool connPool;
	return &connPool;
}

需要注意:以上单例模式是懒汉式单例模式的线程安全版本(通过静态局部变量的初始化来保证线程安全),但构造函数的访问权限为public,外部可以new这个类的实例,这种处理是不太合适的。

在单例模式的实现中,类的构造函数通常会被声明为私有的(在C++中)或者受保护的(在某些支持受保护构造函数的编程语言中),这样可以防止外部代码使用new来创建类的实例。同时,会提供一个静态的公有方法来获取这个唯一实例(可以通过作用域加connection_pool::GetInstance()的方式获取单例),这个方法内部会负责检查实例是否已经被创建,如果还没有创建,则会创建一个新的实例并返回;如果实例已经存在,则直接返回已经创建的实例。

2.4 数据库连接池的初始化

//构造初始化
void connection_pool::init(string url, string User, string PassWord, string DBName, int Port, int MaxConn, int close_log)
{
	m_url = url;
	m_Port = Port;
	m_User = User;
	m_PassWord = PassWord;
	m_DatabaseName = DBName;
	m_close_log = close_log;

url 为服务器通过TCP/IP连接的数据库地址,port 为端口一般为3306,数据库的用户名及密码,以及该账户下创建的数据库的名称,close_log 用来确定是否开启数据集日志(注:服务器日志系统是自定义的log类,与之不同,此处是数据库依赖自带的日志系统)。

	for (int i = 0; i < MaxConn; i++)
	{
		MYSQL *con = NULL;
		con = mysql_init(con);

		if (con == NULL)
		{
			LOG_ERROR("MySQL Error");
			exit(1);
		}
		con = mysql_real_connect(con, url.c_str(), User.c_str(), PassWord.c_str(), DBName.c_str(), Port, NULL, 0);

		if (con == NULL)
		{
			LOG_ERROR("MySQL Error");
			exit(1);
		}
		connList.push_back(con);
		++m_FreeConn;
	}

	reserve = sem(m_FreeConn);

	m_MaxConn = m_FreeConn;
}


循环创建指定数量的数据库句柄,并将其连接上数据库,将成功建立的数据库连接加入连接链表中,同时增加空闲连接计数。sem是对 POSIX 信号量库<semaphore.h>中的信号量操作进行了一层封装,使用上是一致的。最后初始化信号量为空闲连接数量(这里与日志系统的操作不同,日志系统是用POSIX线程库<pthread.h> 的mutex和cond来实现的阻塞队列)。

2.5 池中连接的获取和者释放

//当有请求时,从数据库连接池中返回一个可用连接,更新使用和空闲连接数
MYSQL *connection_pool::GetConnection()
{
	MYSQL *con = NULL;

	if (0 == connList.size())
		return NULL;

	reserve.wait();
	
	lock.lock();

	con = connList.front();
	connList.pop_front();

	--m_FreeConn;
	++m_CurConn;

	lock.unlock();
	return con;
}

reserve.wait() 是对 sem_wait(sem_t *sem) 的一层封装,执行的是将信号量sem的值进行的减1操作,若成功则立即返回执行下一行代码。若sem的值为0,则会阻塞在 sem_wait 处,直到其他线程将sem_post操作将信号量加1,sem_wait 再返回执行下一行代码。

接下来是对连接池(共享资源)进行操作,为了线程安全,需要上锁,随后从连接池的链表队列list <MYSQL *> connList 中取出一条连接,

//释放当前使用的连接
bool connection_pool::ReleaseConnection(MYSQL *con)
{
	if (NULL == con)
		return false;

	lock.lock();

	connList.push_back(con);
	++m_FreeConn;
	--m_CurConn;

	lock.unlock();

	reserve.post();
	return true;
}

reserve.post() 是对 sem_post (sem_t *sem) 的一层封装,执行的是将信号量 sem 的值进行的加1操作,若成功则立即返回执行下一行代码。

2.6 数据库连接池的销毁

//销毁数据库连接池
void connection_pool::DestroyPool()
{

	lock.lock();
	if (connList.size() > 0)
	{
		list<MYSQL *>::iterator it;
		for (it = connList.begin(); it != connList.end(); ++it)
		{
			MYSQL *con = *it;
			mysql_close(con);
		}
		m_CurConn = 0;
		m_FreeConn = 0;
		connList.clear();
	}

	lock.unlock();
}

销毁连接池也需要上锁,防止多线程重复关闭数据库连接引发未定义的行为。list<MYSQL *>::iterator it 定义了STL库 list 容器的迭代器(指针),用以遍历连接池队列,将其一一关闭。

//当前空闲的连接数
int connection_pool::GetFreeConn()
{
	return this->m_FreeConn;
}

connection_pool::~connection_pool()
{
	DestroyPool();
}

2.6 connectionRAII 类的作用

connectionRAII::connectionRAII(MYSQL **SQL, connection_pool *connPool){
	*SQL = connPool->GetConnection();
	
	conRAII = *SQL;
	poolRAII = connPool;
}

connectionRAII::~connectionRAII(){
	poolRAII->ReleaseConnection(conRAII);
}

connectionRAII 类的作用是自动管理数据库连接的获取和释放,使开发者在开发时无需关注连接池的实现。

具体来说,connectionRAII类在构造函数中通过调用连接池的GetConnection()方法获取一个数据库连接,并在析构函数中调用连接池的ReleaseConnection()方法释放该连接。通过这种方式,connectionRAII类确保了在对象不再需要数据库连接时,连接能够被正确地释放回连接池,从而避免了资源泄漏和手动管理的复杂性。

即工作线程,创建一个connectionRAII 的实例对象,这对象在构造函数中调用GetConnection方法获取了一个连接作为其成员变量。在线程用完数据库连接时,直接delete 该对象即可调用析构自动执行将连接放回池内的操作。对于开发人员而言,只需要 new 实例,通过实例的成员变量操作连接,最后delete 实例就可以了,构造和析构函数完成了取连接和放回连接的操作,方便管理。

2.7 该数据库连接池的头文件

#ifndef _CONNECTION_POOL_
#define _CONNECTION_POOL_

#include <stdio.h>
#include <list>
#include <mysql/mysql.h>
#include <error.h>
#include <string.h>
#include <iostream>
#include <string>
#include "../lock/locker.h"
#include "../log/log.h"

using namespace std;

class connection_pool
{
public:
	MYSQL *GetConnection();				 //获取数据库连接
	bool ReleaseConnection(MYSQL *conn); //释放连接
	int GetFreeConn();					 //获取连接
	void DestroyPool();					 //销毁所有连接

	//单例模式
	static connection_pool *GetInstance();

	void init(string url, string User, string PassWord, string DataBaseName, int Port, int MaxConn, int close_log); 

private:
	connection_pool();
	~connection_pool();

	int m_MaxConn;  //最大连接数
	int m_CurConn;  //当前已使用的连接数
	int m_FreeConn; //当前空闲的连接数
	locker lock;
	list<MYSQL *> connList; //连接池
	sem reserve;

public:
	string m_url;			 //主机地址
	string m_Port;		 //数据库端口号
	string m_User;		 //登陆数据库用户名
	string m_PassWord;	 //登陆数据库密码
	string m_DatabaseName; //使用数据库名
	int m_close_log;	//日志开关
};

class connectionRAII{

public:
	connectionRAII(MYSQL **con, connection_pool *connPool);
	~connectionRAII();
	
private:
	MYSQL *conRAII;
	connection_pool *poolRAII;
};

#endif

 

三、工作总结

重新梳理了一遍服务器数据库连接池的实现,写的可能很粗糙,大家将就着看,希望对大家有所帮助,有疑问的欢迎评论区交流。

 

参考文章

数据库的连接池详解_数据库连接池-CSDN博客

Mysql接口API相关函数详细使用说明——mysql_init,mysql_real_connect,mysql_query,mysql_close等相关_mysql的api手册-CSDN博客

Web Server服务器压力测试的研究-CSDN博客

C++实现简易异步LOG日志系统的研究-CSDN博客

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值
>