0


C++ Webserver从零开始:代码书写(十一)——封装数据库连接池

前言

Hello大家好,今晚睡不着,起来写博客了。其实我发现上一篇文章也就是

C++ Webserver从零开始:代码书写(十)——完成Locker类和Log类封装-CSDN博客

的可读性不是很好,这主要原因是因为我写博客主要使用的工具是语雀。而在语雀上编辑完后,复制到csdn上会少很多结构和UI上的东西。比如高亮块,给每个高亮块的颜色区分,代码块命名等。但每次我写完博客后往往处于十分疲惫的阶段,也就直接发上来发布就不管了。现在看起来非常影响可读性,但是我现在实在没有多余的精力去重新排版和规划。我尽量在整个专栏完成之后来一次大的风格统一和整理吧。

数据库连接池

这一节我们来写数据库的连接池,在动手之前我们先看看什么是池;

池是什么

在程序设计思想中,"池"(Pool)通常指的是一种资源管理的模式,其中资源被集中管理并通过预先分配而不是按需创建。这种模式可以用于多种类型的资源,比如:

  • 内存池
  • 线程池
  • 数据库连接池

那么,我们为什么要使用池呢?我们以数据库连接池举例,来说明一下池的优点和好处。

首先,我们先来看看数据库访问的一般流程:

  1. 当系统需要访问数据库时,先系统创建数据库连接
  2. 接着完成数据库操作
  3. 最后断开数据库连接

这非常好理解,就仿佛把大象放入冰箱需要几步一样。但是,从这个流程中我们可以看出,除了第二步,第一步和第三步都是重复且耗时的无意义工作;而且当系统需要频繁地访问数据库时,就会频繁创建和断开数据库连接,这种行为不但耗时,甚至容易对数据库造成安全隐患。因此,我们使用“池”来解决这一问题。

在程序初始化时,我们就立刻创建多个数据连接,把它们集中管理。当程序需要使用时,就从“池”中取出使用,用完再放回池中,这样就避免了频繁的数据库连接和断开操作,而且更加地安全可靠。

池怎么设计

通过上面地介绍,我们发现,其实池是一个装着资源地容器。如果池里装的是进程就是进程池,如果是线程就是线程池,而如果是数据库连接,那就是数据库连接池。具体实现池的方法有许多,比如:数组,链表,队列等。

本节我们使用单例模式链表来实现数据库连接池,同时利用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;
}

数据库访问函数

按照我们的冰箱关大象流程思想,就可以得到数据库访问的函数了,分别是:

  1. 获取数据库连接
  2. 释放数据库连接
  3. 销毁数据库连接

除此之外,我们再设计一个共有函数来获得私有变量空余的连接数量,就完成了数据库访问函数的设计;注意,无论是获取,释放还是销毁,我们都要用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);
}

结束语:

终于,我们完成了数据库连接池的设计,下一章我们开始设计线程池


本文转载自: https://blog.csdn.net/qq_52313711/article/details/136203992
版权归原作者 meeiuliuus 所有, 如有侵权,请联系我们删除。

“C++ Webserver从零开始:代码书写(十一)——封装数据库连接池”的评论:

还没有评论