基于C++“简单且有效”的“数据库连接池”

前言

  • 数据库连接池在开发中应该是很常用的一个组件,他可以很好的节省连接数据库的时间开销;
  • 本文基使用C++实现了一个简单的数据库连接池,代码量只有400行只有,但是压力测试效果很好;
  • 欢迎收藏 + 关注,本人将会持续更新后端与AI算法有关知识点。

文章目录

MySQL 数据库是基于 C/S 模式的,在每一次访问mysql服务器的时候,都需要建立一个TCP连接 ,但是,在高并发情况下,大量的 TCP 三次握手、MySQL Server 连接认证、MySQL Server 关闭连接回收资源和 TCP 四次挥手所耗费的性能事件也是很明显的,故设置连接池就是为了减少这一部分的性能损耗

连接池功能点介绍

初始连接量

表示连接池事先会和 MySQL Serve r创建最小个数的连接,当应用发起 MySQL 访问时,不用再创建和 MySQL Server 新的连接,直接从连接池中获取一个 可用的连接即可,使用完成后并不去释放连接,而是把连接再归还到连接池当中

最大连接量

当并发访问MySQL Server的请求增多时,初始连接量已经不够用了,此时会去创建 更多的连接给应用去使用, 是新创建的连接数量上限是maxSize,不能无限制 的创建连接。并且当这些连接使用完之后,再次归还到连接池当中来维护。

最大空闲时间

当访问MySQL的并发请求多了以后,连接池里面的连接数量会动态增加,上限是maxSize 个,当这些连接用完会再次归还到连接池当中。如果在指定的时间内这些新增的连接都没有被再次使用过,那么新增加的这些连接资源就要被回收掉,只需要保持初始连接量个连接即可

连接超时时间

当MySQL的并发请求量过大,连接池中的连接数量已经到达最大数量了,而此时没有空闲的连接可供使用 ,那么此时应用从连接池获取连接无法成功,它通过阻塞的方式获取连接的时间,如果超过一个时间,那么获取连接失败

连接池主要包含了以下功能点

  • 单例模式设置连接池;
  • 向用户提供一个接口,可以从池中拿到一个数据库连接;
  • 采用生产者-消费者模式,池用队列作为缓冲区,实现生成连接和拿取连接;
  • 采用锁,在创建、拿取连接中进行加锁;
  • 采用智能指针管理从队列中获取的连接,并且采用lambda实现智能指针的析构函数,将连接从新放回队列中;
  • 设置生产连接、回收连接线程,并且驻留后台,作为守护线程;
  • 采用原子变量才记录当前池中的连接数。

创建目录如下

代码

logger.h

cpp 复制代码
#ifndef PUBLIC_H_
#define PUBLIC_H_

#include <iostream>

// 作用:封装简单的LOG
#define LOGGER(str) std::cout << "====》" << __LINE__ << " time: " << __TIME__ << " message: " << str << std::endl;

#endif // !PUBLIC_H_

connection.h

cpp 复制代码
#ifndef CONNECTION_H_
#define CONNECTION_H_

#include <mysql/mysql.h>
#include <ctime>
#include <string>

/*
功能:
    初始化数据库连接
    释放连接
    连接数据库
    查询mysql
    修改数据库数据
    刷新/设置空闲时间的起始时间点
    返回空闲时间
*/

class Connection
{
public:
    // 初始化数据库连接
    Connection();
    // 释放连接
    ~Connection();
    // 连接数据库
    bool connectionSqlBase(std::string ip, unsigned int port, std::string user, std::string passward, std::string dbName);
    // 查询mysql
    MYSQL_RES* query(std::string sql);
    // 修改数据库数据
    bool modify(std::string sql);
    // 刷新/设置空闲时间的起始时间点
    void setStartActivateTime();
    // 返回空闲时间
    clock_t getActivateTime();
private: 
    MYSQL* m_sqlConn{};   // 连接mysql服务器
    clock_t m_activateTime;  // 记录空闲时间的起始点
};

#endif // !CONNECTION_H_

connection.cpp

cpp 复制代码
#include "connection.h"
#include "logger.h"

// 初始化数据库连接
Connection::Connection()
{
    m_sqlConn = mysql_init(nullptr);
    if(m_sqlConn == nullptr) {
        LOGGER("mysql init false !!!");
        return;
    }
}
// 释放连接
Connection::~Connection()
{
    mysql_close(m_sqlConn);
}
// 连接数据库
bool Connection::connectionSqlBase(std::string ip, unsigned int port, std::string user, std::string passward, std::string dbName)
{
    if(nullptr == mysql_real_connect(m_sqlConn, ip.c_str(), user.c_str(), passward.c_str(), dbName.c_str(), port, NULL, 0)) {
        LOGGER("mysql connects error!!");
        return false;
    }
    return true;
}
// 查询mysql
MYSQL_RES* Connection::query(std::string sql)
{
    int res = mysql_query(m_sqlConn, sql.c_str());
    if(0 != res) {
        LOGGER("sql query false!!!");
        return nullptr;
    }
    return mysql_use_result(m_sqlConn);
}
// 修改数据库数据
bool Connection::modify(std::string sql)
{
    int res = mysql_query(m_sqlConn, sql.c_str());
    if(0 != res) {
        LOGGER("sql update/insert/select false!!!");
        return false;
    }
    return true;
}
// 刷新/设置空闲时间的起始时间点
void Connection::setStartActivateTime()
{
    m_activateTime = clock();
}
// 返回空闲时间
clock_t Connection::getActivateTime()
{
    return clock() - m_activateTime;
}

dbConnectionPool.h

cpp 复制代码
#ifndef DBCONNECTION_H_
#define DBCONNECTION_H_

#include "connection.h"
#include <mysql/mysql.h>
#include <queue>
#include <string>
#include <condition_variable>
#include <atomic>
#include <memory>

// 核心:生产者、消费者模式

class DbConnPool
{
public: 
    // 单例模式
    static DbConnPool* getDbConnPool();
    // 对外提供接口: 获取连接的数据库,通过智能指针回收
    std::shared_ptr<Connection> getMysqlConn();
    
    // 测试
    // void test()
    // {
    //     readConfigurationFile();
    // }

private: 
    // 单例模型:构造函数私有化, 目的:创建最小连接数量
    DbConnPool();
    DbConnPool(const DbConnPool&) = delete;
    DbConnPool operator=(const DbConnPool&) = delete;
    
    // 读取配置文件
    bool readConfigurationFile();

    // 如果没有Mysql连接了,则产生新连接,这个线程驻留后台(像守护线程一样)
    void produceNewConn();

    // 如果队列线程 > initSize, 且空闲时间大于最大空闲时间,则回收
    void recycleConn();

private:
    // MYSQL连接信息
    std::string m_ip;
    unsigned int m_port;
    std::string m_username;
    std::string m_password;
    std::string m_dbname;

    // 数据库连接池信息
    int m_initSize;
    int m_maxSize;
    int m_maxFreeTime;
    int m_maxConnTime;

    // 生产者、消费者共享内存:获取连接
    std::queue<Connection*> m_connQueue;
    // 存储当前存储到队列中存储的数量
    std::atomic_int m_conntionCnt{};

    // 锁
    std::mutex m_queueMuetx;

    // 生产者、消费者:产生连接和取连接
    std::condition_variable m_cv;  // 用于生产线程和消费线程之间的通信
};

#endif // !DBCONNECTION_H_

dbConnectionPool.cpp

cpp 复制代码
#include "dbConnectionPool.h"
#include "connection.h"
#include "logger.h"

#include <iostream>
#include <cstdio>
#include <cstring>
#include <mutex>
#include <thread>
#include <functional>
#include <chrono>

// 创建连接:new,但是拿取连接后是交给shared_ptr

// 单例模式
DbConnPool* DbConnPool::getDbConnPool()
{
    // 最简单的方式:static
    static DbConnPool pool;
    return &pool;
}

// 对外提供接口: 获取连接的数据库,通过智能指针回收
std::shared_ptr<Connection> DbConnPool::getMysqlConn()
{
    std::unique_lock<std::mutex> lock(m_queueMuetx);
    // 判断队列是否为空
    while(m_connQueue.empty()) {
        // 队列为空,则等待 最大连接时间, 即这个时候客户端请求连接,但是池里没有连接了,则会等待,如果超过了最大时间,则:连接失败
        if(std::cv_status::timeout == m_cv.wait_for(lock, std::chrono::seconds(m_maxConnTime))) {
            // 再次判断是否为空
            if(m_connQueue.empty()) {
                LOGGER("no conntion !!!!");
                return nullptr;
            }
        }
    }

    /*
    从队列中获取一个连接,交给**智能指针管理**
        注意:删除连接有回收线程监控,而这里获取的连接使用完后,需要还给**队列中**,所以**需要重写智能指针的回收函数**
    */
   std::shared_ptr<Connection> sp(m_connQueue.front(), [&](Connection* pconn){
        // 注意,这里需要加锁
        std::unique_lock<std::mutex> lock(m_queueMuetx);
        pconn->setStartActivateTime();
        m_connQueue.push(pconn);  // 入队
        m_conntionCnt++;   // +1
   });
   // 弹出队列
    m_connQueue.pop();
    m_conntionCnt--;   // -1

    return sp;
}

// 单例模型:构造函数私有化
DbConnPool::DbConnPool()
{
    if(readConfigurationFile() == false) {
        return;
    }

    std::unique_lock<std::mutex> lock(m_queueMuetx);
    for(int i = 0; i < m_maxSize; i++) {
        Connection* newConn = new Connection();
        newConn->connectionSqlBase(m_ip, m_port, m_username, m_password, m_dbname);
        newConn->setStartActivateTime();   // 设置 空闲时间 的起始点
        m_connQueue.push(newConn);      // 入队
        m_conntionCnt++;     // 存储到队列中数据+1
    }

    // 开启线程:检查是否需要需要**新创建连接**
    std::thread produce(std::bind(&DbConnPool::produceNewConn, this));
    produce.detach();   // 驻留后台
    // 开启线程,检查是否需要**删除连接**
    std::thread search(std::bind(&DbConnPool::recycleConn, this));
    search.detach();    // 驻留后台
}

// 读取配置文件
bool DbConnPool::readConfigurationFile()
{
    FILE* fp = fopen("./mysql.ini", "r");
    if(fp == nullptr) {
        LOGGER("mysql.ini open false!!");
        return false;
    }

    char buff[BUFSIZ] = { 0 };

    while(!feof(fp)) {
        // clear
        memset(buff, 0, sizeof(buff));
        // 读取
        fgets(buff, BUFSIZ, fp);
        std::string str = buff;
        // 判空
        if(str.empty()) {
            continue;
        }
        // 截断
        int idx = str.find('=', 0);
        if(idx == -1) {
            continue;
        }
        int end = str.find('\n', idx);
        std::string key = str.substr(0, idx);
        std::string value = str.substr(idx + 1, end - idx - 1);
        //std::cout << "key: " << key << " value: " << value << std::endl;

        if(key == "ip") {
            m_ip = value;
        } else if(key == "port") {
            m_port = atoi(value.c_str());
        } else if(key == "username") {
            m_username = value;
        } else if(key == "password") {
            m_password = value;
        } else if(key == "dbname") {
            m_dbname = value;
        } else if(key == "initSize") {
            m_initSize = atoi(value.c_str());
        } else if(key == "maxSize") {
            m_maxSize = atoi(value.c_str());
        } else if(key == "maxFreeTime") {
            m_maxFreeTime = atoi(value.c_str());
        } else if(key == "maxConnTime") {
            m_maxConnTime = atoi(value.c_str());
        }
    }

    std::cout << m_ip << " " << m_port << " " << m_username << " " << m_password << " " << m_dbname << " " << m_initSize << " " << m_maxSize << " " << m_maxFreeTime << " " << m_maxConnTime << std::endl;

    return true;
}

// 如果池里没有Mysql连接了,则产生新连接,这个线程驻留后台(像守护线程一样)
/*
实现思路:
    设置一个循环:循环检查
        如果队列不为空,则条件变量一直等待
*/
void DbConnPool::produceNewConn()
{
    for(;;) {
        std::unique_lock<std::mutex> lock(m_queueMuetx);
        while(!m_connQueue.empty()) {
            m_cv.wait(lock);    // 条件变量一直等待
        }

        // 这个时候,队列为空,从新创建连接
        for(int i = 0; i < m_maxSize; i++) {
            Connection* newConn = new Connection();
            newConn->setStartActivateTime();   // 刷新时间
            m_connQueue.push(newConn);
            m_conntionCnt++;   // +1
        }

        // 通知等待线程
        m_cv.notify_all();
    }
}

// 如果队列线程 > initSize, 且空闲时间大于最大空闲时间,则回收
void DbConnPool::recycleConn()
{
    for(;;) {
        std::unique_lock<std::mutex> lock(m_queueMuetx);
        while(m_conntionCnt > m_initSize) {
            Connection* conn = m_connQueue.front();
            // 超过最大空闲时间
            if((static_cast<double>(conn->getActivateTime()) / CLOCKS_PER_SEC) > m_maxFreeTime) {
                m_connQueue.pop();
                m_conntionCnt--;
                delete conn;
            } else {   // 对头没超过,则直接退出
                break;
            }
        }
    }
}

压力测试

分别插入10000条数据,对没有使用连接池和使用连接池分别进行测试。

cpp 复制代码
#include "dbConnectionPool.h"
#include "connection.h"
#include <iostream>
#include <mysql/mysql.h>
#include <chrono>

int main() {
    auto start = std::chrono::high_resolution_clock::now(); // 获取当前时间点

    for(int i = 0; i < 10000; i++) {
#if 1
        DbConnPool* pool = DbConnPool::getDbConnPool();
        std::shared_ptr<Connection> conn = pool->getMysqlConn();
        char sql[1024] = { 0 };
        sprintf(sql, "insert into temp(name, age, sex) values('%s', '%d', '%s');", "yxz", 18, "man");
        conn->modify(sql);
#elif  0
        Connection conn;
        conn.connectionSqlBase("127.0.0.1", 3306, "root", "wy2892586", "test");
        char sql[1024] = { 0 };
        sprintf(sql, "insert into temp(name, age, sex) values('%s', '%d', '%s');", "yxz", 18, "man");
        conn.modify(sql);
#endif
    }

    auto end = std::chrono::high_resolution_clock::now(); // 获取结束时间点
    std::chrono::duration<double, std::milli> duration = end - start; // 计算持续时间,并转换为毫秒

    std::cout << "Time: " << duration.count() << " ms" << std::endl;

    return 0;
}

结果如下:

相关推荐
小杜-coding3 分钟前
天机学堂(初始项目)
java·linux·运维·服务器·spring boot·spring·spring cloud
钢铁男儿11 分钟前
深入剖析C#构造函数执行:基类调用、初始化顺序与访问控制
java·数据库·c#
1560820721914 分钟前
在QT中,利用charts库绘制FFT图形
开发语言·qt
姑苏洛言24 分钟前
基于微信公众号小程序的课表管理平台设计与实现
前端·后端
有时间要学习31 分钟前
MySQL——事务
数据库·mysql
小鹭同学_32 分钟前
Java基础 Day27
java·开发语言
烛阴37 分钟前
比UUID更快更小更强大!NanoID唯一ID生成神器全解析
前端·javascript·后端
EdmundXjs39 分钟前
IO Vs NIO
java·开发语言·nio
翻滚吧键盘43 分钟前
Spring Boot,注解,@ComponentScan
java·数据库·spring boot
not coder43 分钟前
Pytest Fixture 详解
数据库·pytest