前言
Hello大家好,今晚睡不着,起来写博客了。其实我发现上一篇文章也就是
C++ Webserver从零开始:代码书写(十)——完成Locker类和Log类封装-CSDN博客
的可读性不是很好,这主要原因是因为我写博客主要使用的工具是语雀。而在语雀上编辑完后,复制到csdn上会少很多结构和UI上的东西。比如高亮块,给每个高亮块的颜色区分,代码块命名等。但每次我写完博客后往往处于十分疲惫的阶段,也就直接发上来发布就不管了。现在看起来非常影响可读性,但是我现在实在没有多余的精力去重新排版和规划。我尽量在整个专栏完成之后来一次大的风格统一和整理吧。
数据库连接池
这一节我们来写数据库的连接池,在动手之前我们先看看什么是池;
池是什么
在程序设计思想中,"池"(Pool)通常指的是一种资源管理的模式,其中资源被集中管理并通过预先分配而不是按需创建。这种模式可以用于多种类型的资源,比如:
- 内存池
- 线程池
- 数据库连接池
那么,我们为什么要使用池呢?我们以数据库连接池举例,来说明一下池的优点和好处。
首先,我们先来看看数据库访问的一般流程:
- 当系统需要访问数据库时,先系统创建数据库连接
- 接着完成数据库操作
- 最后断开数据库连接
这非常好理解,就仿佛把大象放入冰箱需要几步一样。但是,从这个流程中我们可以看出,除了第二步,第一步和第三步都是重复且耗时的无意义工作;而且当系统需要频繁地访问数据库时,就会频繁创建和断开数据库连接,这种行为不但耗时,甚至容易对数据库造成安全隐患。因此,我们使用“池”来解决这一问题。
在程序初始化时,我们就立刻创建多个数据连接,把它们集中管理。当程序需要使用时,就从“池”中取出使用,用完再放回池中,这样就避免了频繁的数据库连接和断开操作,而且更加地安全可靠。
池怎么设计
通过上面地介绍,我们发现,其实池是一个装着资源地容器。如果池里装的是进程就是进程池,如果是线程就是线程池,而如果是数据库连接,那就是数据库连接池。具体实现池的方法有许多,比如:数组,链表,队列等。
本节我们使用单例模式和链表来实现数据库连接池,同时利用RAII机制来释放数据连接;
单例模式代码
老朋友了,在上一章Log日志系统的设计中我们就使用了单例模式,这里不再赘述。
我们上来就是个私有构造,再用公有静态方法获得唯一实例,里面用上局部静态变量保证了线程安全,很快啊,单例模式就写好了;
class connection_pool{
public:
static connection_pool* Getinstance() {
static connection_pool connPool;
return &connPool;
}
private:
connection_pool();
~connection_pool();
};
再把构造函数和析构函数补充完整,里面初始化的成员不要着急,后面会写;
connection_pool::connection_pool() {
m_FreeConn = 0;
m_CurConn = 0;
}
connection_pool::~connection_pool() {
DestroyPool();
}
数据库连接池初始化
数据库的资源我们使用信号量进行同步,所以,将信号量初始化为数据库的连接总数;
初始化的代码十分简单,只有两个不常见的API需要了解
mysql_init
mysql_real_connection
同时,细心同学可以观察一下这里的进行了我们第一次的Log日志使用,根据之前设计的Log日志系统,体会其运行的逻辑。
public:
/*初始化数据库连接池*/
void init(string url, string User, string PassWord, string DBName, int MaxConn, int Port, int close_log);
private:
int m_MaxConn;//最大连接数
int m_FreeConn;//可用连接数
int m_CurConn;//已用连接数
list<MYSQL *> connList;//连接池
locker m_lock;
sem reserve;//信号量记录可用资源
public:
string m_url;//主机地址
string m_Port;//数据库端口号
string m_User;//数据库用户名
string m_PassWord;//数据库密码
string m_DatabaseName;//数据库名
int m_close_log;//是否开启日志
void connection_pool::init(string url, string User, string PassWord, string DBName, int MaxConn, int Port, int close_log) {
m_url = url;
m_Port = Port;
m_User = User;
m_PassWord = PassWord;
m_DatabaseName = DBName;
m_close_log = close_log;
for (int i = 0; i < MaxConn; ++i) {
MYSQL *con = NULL;
con = mysql_init(con);
if (con == nullptr) {
LOG_ERROR("MySQL Error : mysql_init");
exit(1);
}
/*真正的连接函数*/
con = mysql_real_connect(con, url.c_str(), User.c_str(), PassWord.c_str(), DBName.c_str(), Port, NULL, 0);
if (con == nullptr) {
LOG_ERROR("MySQL Error : mysql_real_connect");
exit(1);
}
connList.push_back(con);
m_FreeConn++;
}
reserve = sem(m_FreeConn);//信号量记录共享资源总量
m_MaxConn = m_FreeConn;
}
数据库访问函数
按照我们的冰箱关大象流程思想,就可以得到数据库访问的函数了,分别是:
- 获取数据库连接
- 释放数据库连接
- 销毁数据库连接
除此之外,我们再设计一个共有函数来获得私有变量空余的连接数量,就完成了数据库访问函数的设计;注意,无论是获取,释放还是销毁,我们都要用mutex来保证线程同步;同时,获取连接前需要wait()阻塞等到临界区有资源,释放连接后需要post()来提醒其他线程临界区有新资源
public:
MYSQL *Getconnection(); //获取数据库连接
bool ReleaseConnection(MYSQL *conn); //释放数据库连接
int GetFreeConn(); //获得空余连接数量
void DestroyPool(); //销毁所有连接
MYSQL *connection_pool::Getconnection() {
MYSQL * con = NULL;
if (connList.size() == 0) {
return NULL;
}
reserve.wait();
m_lock.lock();
con = connList.front();
connList.pop_front();
m_FreeConn--;
m_CurConn++;
m_lock.unlock();
return con;
}
bool connection_pool::ReleaseConnection(MYSQL *con) {
if (con == nullptr) {
return false;
}
m_lock.lock();
connList.push_back(con);
m_FreeConn++;
m_CurConn--;
m_lock.unlock();
reserve.post();
return true;
}
int connection_pool::GetFreeConn() {
return m_FreeConn;
}
void connection_pool::DestroyPool() {
m_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_FreeConn = 0;
m_CurConn = 0;
connList.clear();
}
m_lock.unlock();
}
RAII类
RAII(Resource Acquisition Is Initialization)(资源获取即初始化)是一种C++编程中的重要设计原则,用于管理资源的获取和释放。RAII的核心思想是通过对象的生命周期来控制资源的生命周期,从而确保资源在合适的时候被正确地获取和释放。我们的智能指针如unique_ptr,锁lock_gurad和文件流都采用了RAII的机制
根据上述思想,我们单独再创建一个RAII类,这个类的唯一作用就是与数据库连接池的资源进行绑定;可以看到这个类中只有构造函数和析构函数。
这样当类创建实例时就会调用构造函数,构造函数内就会调用数据库连接函数;当类的生命周期结束时就会调用析构函数,析构函数会调用销毁数据库函数;从而实现了资源的获取与释放与类的实例的生命周期绑定。
/*使用RAII技术来保证connPool单例对象的生命周期符合RAII规则*/
class connectionRAII {
public:
connectionRAII(MYSQL **con, connection_pool *connPool);
~connectionRAII();
private:
connection_pool *poolRALL;
MYSQL *conRAII;
};
connectionRAII::connectionRAII(MYSQL **SQL, connection_pool *connPool) {
*SQL = connPool->Getconnection();
conRAII = *SQL;
poolRALL = connPool;
}
connectionRAII::~connectionRAII() {
poolRALL ->ReleaseConnection(conRAII);
}
结束语:
终于,我们完成了数据库连接池的设计,下一章我们开始设计线程池