基于c++的数据库连接池 的实现与理解

前言:

为了加深对c++多线程和mysql的理解,为了做了这个小型项目。代码参考了github上和CSDN及B站上很多人的讲解。做完之后发现这个项目“麻雀虽小五脏俱全”,特此记录下自己的理解和编程过程。

1.项目目的

在高并发的情况,大量的TCP三次握手,MySQL server连接认证,MySQL server连接关闭回收资源,TCP四次挥手会耗费性能。本项目的目的是为了避免频繁的向数据库申请资源,释放资源带来性能损耗。
缺点:
1.维护连接池这个对象需要占用一定的内存空间。
2.数据库连接池中可能存在着多个没有被使用的连接一直连接着数据库(这意味着资源的浪费)

2.基本思路

为数据库的连接建立一个缓存池,预先在该缓存中放入一定数量的连接。当多个任务需要访问mysql时,不需要每个任务都去直接通过TCP连接mysql server,而是在该缓存池中取对应数量的连接即可。用完之后不需要释放该连接,只需要归还到连接池即可。

3关键点分析

1.利用mysql提供的api,自定义一个“连接”类。后面把该连接类放入容器中作为连接池。
2.基于上述分析,连接池的设计采用单例模式设计。
3.拟采用生产者-消费者线程模型,生产者负责产生连接,消费者负责使用连接。考虑并发情况,使用互斥锁和条件变量实现线程安全和同步,即:生产后再消费的同步
4.实现连接池的容器考虑队列实现。在并发情况下,STL的queue不是线程安全的,可使用互斥锁实现线程安全。
5. 由于连接用完后是归还而不是释放,拟采用智能指针来管理连接,用lamda表达式来实现连接归还的功能。(因为智能指针出作用域自动析构,且申明指针智能时可以指定删除器,方便自定义归还功能)
6. 连接池中连接的数量为多少时性能最佳?

4 代码实现

4.1 “连接” 类的功能

分析可知,连接池中的“连接” 使用类实现。利用mysql提供的API可实现。
主要功能包括:
1.“连接”的构造和析构功能
2.连接数据库
3.对数据库的操作
4. 返回一个连接的空闲时间(用于释放多余产生的连接,后文会说明)

4.2“连接” 类的代码如下

注:(头文件是类的定义,源文件是类中成员方法的实现):

Connection.h
#pragma once

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

using namespace std;

class Connection
{
public:
    // 初始化数据库连接
    Connection();

    // 释放数据库连接资源
    ~Connection();

    // 连接数据库
    bool connect(string ip,
        unsigned short port,
        string username,
        string password,
        string dbname);

    // 更新操作 insert、delete、update
    bool update(string sql);

    // 查询操作 select
    MYSQL_RES* query(string sql);

    // 刷新连接的起始空闲时刻
    // 记录每个队列的空闲时间,缓解服务器资源,在入队时
    void refreshAliveTime();

    // 返回连接空闲的时长
    clock_t getAliveTime();

private:
    MYSQL* _conn; // 表示和MySQL Server的一条连接
    clock_t _alivetime; // 记录进入空闲状态后的起始存活时刻(即在队列中出现的时刻)
};
Connection.cpp
#include "public.h"
#include "Connection.h"

// 初始化数据库连接
Connection::Connection()
{
    _conn = mysql_init(nullptr);
}

// 释放数据库连接资源
Connection::~Connection()
{
    if (_conn != nullptr)
        mysql_close(_conn);
}

// 连接数据库
bool Connection::connect(string ip,
    unsigned short port,
    string username,
    string password,
    string dbname)
{
    MYSQL* p = mysql_real_connect(_conn, ip.c_str(), username.c_str(),
        password.c_str(), dbname.c_str(),
        port, nullptr, 0);

    //mysql_query(_conn, "set interactive_timeout=24*3600");

    return p != nullptr;
}

// 更新操作 insert、delete、update
bool Connection::update(string sql)
{
    bool a = mysql_query(_conn, sql.c_str());
    if (mysql_query(_conn, sql.c_str()))
    {
        LOG("更新失败:" + sql + "\nmysql_error:" + mysql_error(_conn));
        return false;
    }
    return true;
}

// 查询操作 select
MYSQL_RES* Connection::query(string sql)
{
    // 查询操作 select
    // 如果查询成功,返回0。如果出现错误,返回非0值

    if (mysql_query(_conn, sql.c_str()))
    {
        LOG("查询失败" + sql + "\nmysql_error:" + mysql_error(_conn));
        return nullptr;
    }
    return mysql_use_result(_conn);
}

// 刷新连接的起始空闲时刻
void Connection::refreshAliveTime()
{
    _alivetime = clock();
}

// 返回连接空闲的时长
clock_t Connection::getAliveTime()
{
    return clock() - _alivetime;
}
4.3连接池的功能
连接池的主要参数:

1.初始连接数:连接池事先会准备一些连接备用。最小连接数需要根据实际情况不断测试决定,设置太多的话会出现很多空连接,浪费资源。
2.最大连接数:当并发请求太多了之后,初始量不够用了。这时候会根据需求创建更多的连接,但不能无限创建,因为考虑到资源浪费问题。
3.最大空闲时间: 当并发请求增多以后,连接数会变多。由于“归还”原因,这些连接不会被直接释放,而是归还到队列中。假设后面的并发请求没那么多,那么之前产生的多的连接会造成资源冗余浪费。需要我们设置一个最大空闲时间。如果在最大空闲时间内,该连接还没有被使用的话,就需要被回收掉,节约资源。考虑容器基于队列实现,当队头元素的存活时间都没超过最大空闲时间的话,后面的连接肯定也没超过该最大空闲时间。
4.连接超时时间: 当并发请求太多了,且连接池的连接数已经超过最大连接数了,导致已经没有空闲的连接可以使用了。那么此时线程请求再连接会失败。此时需设置一个连接超时时间,如果超时了,那么获取失败,无法连接数据库。

待实现的连接池的主要功能如下:

1.创建一个连接池对象。(因为是一个单例模式)
2.初始化连接数以及生产新连接(生产过程是连接池类内部多线程创建的,所以权限为private;另外需要定义一个连接数计算器,使用原子变atomic,就不需要用互斥锁来保护该计数器了)
3.从连接池中获取一个可用连接(消费过程是用户请求,权限为public;用完后归还到队列中)
4.回收连接(通过定义一个扫描函数,获取每个连接的空闲时间,用于多余连接的释放)
5.加载初始配置项,主要是数据库连接参数如用户名密码等(可选)。

4.4 连接池的代码如下:
CommonConnectionPool.h
#pragma once

#include "Connection.h"
#include <string>
#include <queue>
#include <mutex>
#include <atomic>
#include <thread>
#include <memory>
#include <functional>
#include <condition_variable>

using namespace std;

// 实现连接池功能模块


class ConnectionPool
{
public:
    // 获取连接池对象实例(懒汉式单例模式,在获取实例时才实例化对象)
    static ConnectionPool* getConnectionPool();
    // 给外部提供接口,从连接池中获取一个可用的空闲连接
    //注意,这里不要直接返回指针,否则我们还需要定义一个(归还连接)的方法,还要自己去释放该指针。
    //这里直接返回一个智能指针,智能指针出作用域自动析构,(我们只需重定义析构即可--不释放而是归还) 
    shared_ptr<Connection> getConnection();
private:
    // 单例模式——构造函数私有化
    ConnectionPool();
    // 从配置文件中加载配置项
    bool loadConfigFile();

    // 运行在独立的线程中,专门负责生产新连接
    // 非静态成员方法,其调用依赖对象,要把其设计为一个线程函数,需要绑定this指针。 
    // 把该线程函数写为类的成员方法,最大的好处是 非常方便访问当前对象的成员变量。(数据)
    void produceConnectionTask();

    // 扫描超过maxIdleTime时间的空闲连接,进行对于连接的回收
    void scannerConnectionTask();

    string _ip;                    // MySQL的ip地址
    unsigned short _port;                  // MySQL的端口号,默认为3306
    string _username;              // MySQL登陆用户名
    string _password;              // MySQL登陆密码
    string _dbname;                // 连接的数据库名称
    int _initSize;              // 连接池的最大初始连接量
    int _maxSize;               // 连接池的最大连接量
    int _maxIdleTime;           // 连接池的最大空闲时间
    int _connectionTimeout;     // 连接池获取连接的超时时间

    // 存储MySQL连接的队列
    queue<Connection*> _connectionQue;
    // 维护连接队列的线程安全互斥锁
    mutex _queueMutex;

    // 记录connection连接的总数量
    atomic_int _connectionCnt;

    // 设置条件变量,用于连接生产线程和连接消费线程的通信
    condition_variable cv;
};

单例模式的设计:
Windows系统回收站窗口。,也就是说在整个系统运行的过程中,系统只维护一个回收站的实例。这就是一个典型的单例模式运用。
实现方法:
(1)将构造方法私有化,使其不能在类的外部通过new关键字实例化该类对象。
(2)在该类内部产生一个唯一的实例化对象,并且将其封装为private static类型。
(3)定义一个静态方法返回这个唯一对象。

在饿汉模式中,类被加载时,就会实例化一个对象并交给自己的引用,供系统使用;而且,由于这个类在整个生命周期中只会被加载一次,因此只会创建一个实例,即能够充分保证单例。

我们从懒汉式单例可以看到,单例实例被延迟加载,即只有在真正使用的时候才会实例化一个对象并交给自己的引用。

优点:由于在系统内存中只存在一个对象,因此可以 节约系统资源,当 需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。

滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出。

STL 中的queue不是线程安全的,子线程对queue队列做push操作,同时主线程对queue执行pop操作,则可能会发生异常。

CommonConnectionPool.cpp
#include "CommonConnectionPool.h"
#include "public.h"

// 线程安全的懒汉单例函数接口
ConnectionPool* ConnectionPool::getConnectionPool()
{
    // 对于静态局部变量的初始化,编译器自动进行lock和unlock
    static ConnectionPool pool;
    return &pool;
}

// 从配置文件中加载配置项
bool ConnectionPool::loadConfigFile()
{
    FILE* pf = fopen("mysql.ini", "r");
    if (pf == nullptr)
    {
        LOG("File 'mysql.ini' is not existing!");
        return false;
    }

    // 逐行处理配置文件中的配置字符串
    while (!feof(pf))
    {
        // 配置字符串举例:username=root\n

        // 从文件中获取一行配置字符串
        char line[1024] = { 0 };
        fgets(line, 1024, pf);
        string str = line;

        // 找到配置字符串中的'='
        int idx = str.find('=', 0);

        // 无效的配置项
        if (idx == -1)
        {
            // 当配置字符串中找不到'='时说明该配置字符串有问题或者是注释,将其忽略
            continue;
        }

        // 分别取出该行配置中的key和value
        int endIdx = str.find('\n', idx);
        string key = str.substr(0, idx);
        string value = str.substr(idx + 1, endIdx - idx - 1);

        if (key == "ip")
        {
            _ip = value;
        }
        else if (key == "port")
        {
            _port = atoi(value.c_str());
        }
        else if (key == "username")
        {
            _username = value;
        }
        else if (key == "password")
        {
            _password = value;
        }
        else if (key == "dbname")
        {
            _dbname = value;
        }
        else if (key == "maxSize")
        {
            _maxSize = atoi(value.c_str());
        }
        else if (key == "maxIdleTime")
        {
            _maxIdleTime = atoi(value.c_str());
        }
        else if (key == "connectionTimeout")
        {
            _connectionTimeout = atoi(value.c_str());
        }
        else if (key == "initSize")
        {
            _initSize = atoi(value.c_str());
        }

    }

    return true;
}

// 连接池的构造函数
ConnectionPool::ConnectionPool()
{
    // 加载配置项
    if (!loadConfigFile())
    {
        return;
    }

    // 创建初始数量的连接
    for (int i = 0; i < _initSize; ++i)
    {
        Connection* p = new Connection();
        p->connect(_ip, _port, _username, _password, _dbname);
        p->refreshAliveTime(); // 记录连接的起始空闲时刻
        _connectionQue.push(p);
        _connectionCnt++;
    }

    // 启动一个新的线程,作为连接的生产者
    thread produce(std::bind(&ConnectionPool::produceConnectionTask, this));
    produce.detach();   //守护线程

    // 启动一个新的定时线程,扫描超过maxIdleTime时间的空闲连接,并对其进行回收
    thread scanner(std::bind(&ConnectionPool::produceConnectionTask, this));
    scanner.detach();
}

// 运行在独立的线程中,专门负责产生新连接
void ConnectionPool::produceConnectionTask()
{
    for (;;)
    {
        unique_lock<mutex> lock(_queueMutex); //条件变量需要和互斥锁一块使用
        while (!_connectionQue.empty())
        {
            // 队列非空时,此处生产线程进入等待状态
            cv.wait(lock); //进入等待时,释放锁,保证消费者线程正常运行
        }
        // 连接数量没有到达上限,继续创建新的连接
        if (_connectionCnt < _maxSize)
        {
            Connection* p = new Connection();
            p->connect(_ip, _port, _username, _password, _dbname);
            _connectionQue.push(p);
            _connectionCnt++;
        }
        // 通知消费者线程,可以消费连接了
        cv.notify_all();
    }
}

// 给外部提供接口,从连接池中获取一个可用的空闲连接
shared_ptr<Connection> ConnectionPool::getConnection()
{
    unique_lock<mutex> lock(_queueMutex);
    while (_connectionQue.empty())
    {
        if (cv_status::timeout == cv.wait_for(lock, std::chrono::milliseconds(_connectionTimeout))) //超时唤醒
        {
            if (_connectionQue.empty())
            {
                LOG("Failed to get connection:got idle connection timeout!");
                return nullptr;
            }
        }
    }

    /*
     * shared_ptr智能指针析构时,会把connection资源直接delete掉,
     * 相当于调用connection的析构函数,connection就被close掉了。
     * 这里需要自定义shared_ptr的释放资源的方式,把connection直接归还到queue当中*/
    shared_ptr<Connection> sp(_connectionQue.front(),
        [&](Connection* pcon)
        {
            // 这里是在服务器应用线程中调用的,所以一定要考虑队列的线程安全操作
            unique_lock<mutex> lock(_queueMutex);
            pcon->refreshAliveTime(); //在归还回空闲连接队列之前要记录一下连接开始空闲的时刻
            _connectionQue.push(pcon);
        });

    _connectionQue.pop();

    // 消费者取出一个连接之后,通知生产者,生产者检查队列,如果为空则生产
    cv.notify_all();

    return sp;
}

// 扫描超过maxIdleTime时间的空闲连接,进行对于连接的回收
void ConnectionPool::scannerConnectionTask()
{
    for (;;)
    {
        // 通过sleep实现定时效果
        this_thread::sleep_for(chrono::seconds(_maxIdleTime));

        // 扫描整个队列,释放多余的连接
        unique_lock<mutex> lock(_queueMutex);
        while (_connectionCnt > _initSize)
        {
            Connection* p = _connectionQue.front();
            if (p->getAliveTime() >= (_maxIdleTime * 1000))
            {
                _connectionQue.pop();
                _connectionCnt--;
                delete p; // 调用~Connection()释放连接
            }
            else
            {
                // 队头的连接没有超过_maxIdleTime,其它连接肯定也没有
                break;
            }
        }
    }
}

其实生产者与消费者模式就是一个多线程并发协作的模式,在这个模式中呢,一部分线程被用于去生产数据,另一部分线程去处理数据,于是便有了形象的生产者与消费者了。而为了更好的优化生产者与消费者的关系,便设立一个缓冲区,也就相当于一个数据仓库,当生产者生产数据时锁住仓库,不让消费者访问,当消费者消费时锁住仓库,不让生产者访问仓库。

4.5 配置文件 mysql.ini
ip=127.0.0.1
port=3306
username=root
password=111111
dbname=chat
initSize=10
maxSize=1024
maxIdleTime=60
connectionTimeout=100

5.测试函数 main.cpp及结果

#pragma once
#include<iostream>
#include <string>
#include <ctime>
#include "Connection.h"
#include "CommonConnectionPool.h"
int n = 10000;//数据量
int main()
{
	


	//不使用连接池,单线程:
	clock_t begin = clock();
	for (int i = 0; i < n; i++)
	{
		Connection conn;
		char sql[1024] = { 0 };
		sprintf(sql, "insert into t1(id,name) values(%d,'%s')",
		1, "a");
		conn.connect("127.0.0.1", 3306, "root", "zh601572", "chat");
		conn.update(sql); 
	}
	clock_t end = clock();
	cout << end - begin << "ms" << endl;
	return 0;


	//不使用连接池,4线程:
	//Connection conn;
	//conn.connect("localhost", 3306, "root", "zh601572", "chat");
	//clock_t begin = clock();
	//thread t1([]()
	//	{
	//		for (int i = 0; i < n/4; ++i)
	//		{
	//			Connection conn;
	//			char sql[1024] = { 0 };
	//			sprintf(sql, "insert into t1(id,name) values(%d,'%s')",
	//				5, "a");
	//			conn.connect("localhost", 3306, "root", "zh601572", "chat");
	//			conn.update(sql);
	//		}
	//	});
	//thread t2([]()
	//	{
	//		for (int i = 0; i < n / 4; ++i)
	//		{
	//			Connection conn;
	//			char sql[1024] = { 0 };
	//			sprintf(sql, "insert into t1(id,name) values(%d,'%s')",
	//				6, "a");
	//			conn.connect("localhost", 3306, "root", "zh601572", "chat");
	//			conn.update(sql);
	//		}
	//	});
	//thread t3([]()
	//	{
	//		for (int i = 0; i < n / 4; ++i)
	//		{
	//			Connection conn;
	//			char sql[1024] = { 0 };
	//			sprintf(sql, "insert into t1(id,name) values(%d,'%s')",
	//				7, "a");
	//			conn.connect("localhost", 3306, "root", "zh601572", "chat");
	//			conn.update(sql);
	//		}
	//	});
	//thread t4([]()
	//	{
	//		for (int i = 0; i < n / 4; ++i)
	//		{
	//			Connection conn;
	//			char sql[1024] = { 0 };
	//			sprintf(sql, "insert into t1(id,name) values(%d,'%s')",
	//				8, "a");
	//			conn.connect("localhost", 3306, "root", "zh601572", "chat");
	//			conn.update(sql);
	//		}
	//	});
	//t1.join();
	//t2.join();
	//t3.join();
	//t4.join();
	//clock_t end = clock();
	//cout << end - begin << "ms" << endl;
	//return 0;



	//使用连接池,单线程:
	//clock_t begin = clock();
	//ConnectionPool* cp = ConnectionPool::getConnectionPool();
	//for (int i = 0; i < n; i++)
	//{
	//	shared_ptr<Connection> sp = cp->getConnection();
	//	char sql[1024] = { 0 };
	//	sprintf(sql, "insert into t1(id,name) values(%d,'%s')",
	//	4, "zhouhui");
	//	sp ->update(sql);
	//}
	//clock_t end = clock();
	//cout << end - begin << "ms" << endl;
	//return 0;


	//使用连接池,四线程:
	//clock_t begin = clock();
	//
	//thread t1([]()
	//	{
	//		ConnectionPool* cp = ConnectionPool::getConnectionPool();
	//		for (int i = 0; i < n / 4; i++)
	//		{
	//			shared_ptr<Connection> sp = cp->getConnection();
	//			char sql[1024] = { 0 };
	//			sprintf(sql, "insert into t1(id,name) values(%d,'%s')",4, "zhouhui");
	//			sp ->update(sql);
	//		}
	//	}
	//);

	//thread t2([]()
	//	{
	//		ConnectionPool* cp = ConnectionPool::getConnectionPool();
	//		for (int i = 0; i < n /4; i++)
	//		{
	//			shared_ptr<Connection> sp = cp->getConnection();
	//			char sql[1024] = { 0 };
	//			sprintf(sql, "insert into t1(id,name) values(%d,'%s')", 4, "zhouhui");
	//			sp->update(sql);
	//		}
	//	}
	//);

	//thread t3([]()
	//	{
	//		ConnectionPool* cp = ConnectionPool::getConnectionPool();
	//		for (int i = 0; i < n / 4; i++)
	//		{
	//			shared_ptr<Connection> sp = cp->getConnection();
	//			char sql[1024] = { 0 };
	//			sprintf(sql, "insert into t1(id,name) values(%d,'%s')", 4, "zhouhui");
	//			sp->update(sql);
	//		}
	//	}
	//);
	//thread t4([]()
	//	{
	//		ConnectionPool* cp = ConnectionPool::getConnectionPool();
	//		for (int i = 0; i < n / 4; i++)
	//		{
	//			shared_ptr<Connection> sp = cp->getConnection();
	//			char sql[1024] = { 0 };
	//			sprintf(sql, "insert into t1(id,name) values(%d,'%s')", 4, "zhouhui");
	//			sp->update(sql);
	//		}
	//	}
	//);

	//t1.join();
	//t2.join();
	//t3.join();
	//t4.join();

	//clock_t end = clock();
	//cout << (end - begin) << "ms" << endl;

	//return 0;








}

结果如下
在这里插入图片描述
可以看到还是节约很多时间资源的。

6 后记:

6.1 配置

(x86对应32位机,我的是64位)
关于项目的配置。(mysql文件的路径一般在c盘的program file下)而不是(program file x86下)同理:下面图所示也改成x64.在这里插入图片描述我这里是vs2019和mysql5.7。配置如下:
1.右击项目:c/c++ 常规 附加包含目录 填入mysql.h 头文件的路径
2.右击项目:链接器 常规 附加库目录 填入libmysql.lib 的路径
3.右击项目:链接器 输入 附加依赖项 填入libmysql.lib 的名字
4.把文件 libmysql.dll 复制到自己所在的项目文件内。

6.2 关于连接池连接数量的设置问题

暂时还不太懂,放个文章连接在这,后面慢慢思考。
数据库连接池设置多少连接才合适?

注意开始时要启动mysql 服务:
service mysqld restart

  • 0
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

通信仿真爱好者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值