前言
近期准备秋招,于是将之前的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> 的使用
参考日志系统中对线程库的使用说明:
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
三、工作总结
重新梳理了一遍服务器数据库连接池的实现,写的可能很粗糙,大家将就着看,希望对大家有所帮助,有疑问的欢迎评论区交流。
参考文章
Mysql接口API相关函数详细使用说明——mysql_init,mysql_real_connect,mysql_query,mysql_close等相关_mysql的api手册-CSDN博客