引言
本篇文章介绍一个四大池中的数据库连接池,里面会涉及到信号队列,智能指针,对数据库操作的二次封装(c接口)等一些基本的操作,实现连接池的四个必备的组成部分:初始连接量,最大连接量,最大空闲时间,和超时连接时间。
简介
我们为什么要数据库连接池,解决的问题主要是高并发的场景。当同一时间大量的连接请求访问数据库的时候,如果没有数据库连接池,那么我们的数据库就会创建上万个socket来和客户端进行连接,这对于性能的消耗是非常大的。而且对于TCP来说,我们需要有三次握手和四次挥手,这种耗时的操作也会导致网络的卡顿,每当数据的发出我们都需要三次握手和四次挥手。所以我们参考线程池的特点,我们可以创建一个队列,来存储一定量的connection,当有客户端访问的时候,我们的connection可以直接分配,这样子我们我们只需要建立依次连接,拿到对应的connection,之后发送的信息全部都可以通过已有的连接发送信息。
当客户量极大幅度增加的时候,我们就需要扩容我们的连接池,但是扩容是有一个限度的,我们记作maxConnectionSize,但是当用户量变少之后,我们不再需要这么多connection,我们就要减少这些数量,恢复到刚开始,那这个恢复的条件就是一个计时的过程,我们这里可以设置成100ms。
当客户量下降了之后,我们就不再需要这么多的连接量了,所以这个时候我们通过计时来判断什么时候可以把connection的数量恢复到最初值。
最后我们的连接断开之后我们需要回到队列里面,所以我们需要重写智能指针的析构函数。
代码
首先我们写一个处理数据库c接口的一个模板,除此之外,对于每一个连接,我们都需要设置几个函数来处理如果一段时间之后连接池connection数量恢复到初始化,一个是得到连接的起始值,一个是记录这个连接最新存在的时间。
#pragma once #ifndef _CONNECTION_H_ #define _CONNECTION_H_ #include <mysql.h> #include <string> #include <chrono> namespace Litguidyo { class Connection { public: Connection(); ~Connection(); bool connect(std::string ip, unsigned short port, std::string user, std::string password, std::string dbname); bool update(std::string sql); MYSQL_RES* query(std::string sql); void refreshAliveTime() { _aliveTime = std::chrono::steady_clock::now(); } std::chrono::milliseconds getAliveTime(); private: MYSQL* _connection; // MySQL和Server的一个链接 std::chrono::steady_clock::time_point _aliveTime; // 记录进入空闲状态的存活时间 _ }; } #endif#include "Connection.h" #include "../Util/Helper.h" Litguidyo::Connection::Connection() { _connection = mysql_init(nullptr); } Litguidyo::Connection::~Connection() { if (_connection != nullptr) { mysql_close(_connection); } } bool Litguidyo::Connection::connect(std::string ip, unsigned short port, std::string user, std::string password, std::string dbname) { MYSQL* p = mysql_real_connect(_connection, ip.c_str(), user.c_str(), password.c_str(), dbname.c_str(), port, nullptr, 0); return p != nullptr; } bool Litguidyo::Connection::update(std::string sql) { if (mysql_query(_connection, sql.c_str())) { // LOG("update failed" + sql); return false; } return true; } MYSQL_RES* Litguidyo::Connection::query(std::string sql) { if (mysql_query(_connection, sql.c_str())) { LOG("query failed" + sql); return nullptr; } return mysql_use_result(_connection); } std::chrono::milliseconds Litguidyo::Connection::getAliveTime() { return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - _aliveTime); }我们这里使用单例模式(懒汉模式),然后连接池里面记录了数据库的所有基本的信息,方便客户端的连接。
我们扩展连接数量的方式采用的是生产者-消费者模型,引入的是消息队列
#pragma once #include <queue> #include "../Connection/Connection.h" #include <vcruntime.h> #include <mutex> #include <atomic> #include <thread> #include <condition_variable> #include <memory> #ifndef _CONNECTIONPOOL_H_ #define _CONNECTIONPOOL_H_ namespace Litguidyo { class ConnectionPool { private: ConnectionPool(); ConnectionPool(std::string& path); ConnectionPool(const ConnectionPool&) = delete; ConnectionPool& operator=(const ConnectionPool&) = delete; public: static ConnectionPool* getInstance(); std::shared_ptr<Connection> getConnection(); private: std::string _ip; std::string _username; std::string _password; unsigned short _port; // 3306 size_t _initConnectionSize; size_t _maxConnectionSize; size_t _maxIdleTime; // 连接池最大空闲时间 size_t _connectionTimeout; // 连接池最大超时时间 std::string _dbname; // 数据库名 std::queue<Connection*> _connectionQueue; std::mutex _queueMutex; // 维护队列线程安全 std::atomic<size_t> _currentConnectionSize; // 当前连接池中连接数 bool loadConfigPool(); void produceConnectionTask(); // 运行在独立线程中,专门负责生成新连接 std::condition_variable cv; // 设置条件变量,用于连接生产线程和消费线程的通信 void scannerConnectionTask(); // 运行在独立线程中,专门负责扫描超过maxIdleTime时间的空闲连接,进行对于连接的回收 }; } #endif初始化先创建少量的连接数量,同时构建两个线程,一个线程专门创建连接为了应对connection不够的情况,一个线程专门扫描队列,把那些超时的连接全部断开然后重新放入队列里面。
Litguidyo::ConnectionPool::ConnectionPool() { if (!loadConfigPool()) { return; } for (size_t i = 0; i < _initConnectionSize; i++) { Connection* connection = new Connection(); connection->connect(_ip, _port, _username, _password, _dbname); connection->refreshAliveTime(); // 刷新起始时间 _connectionQueue.push(connection); _currentConnectionSize++; } std::thread produce(std::bind(&ConnectionPool::produceConnectionTask, this)); produce.detach(); // 启动一个新的线程,扫描超过,maxIdleTime时间的空闲连接,进行对于连接的回收 std::thread scanner(std::bind(&ConnectionPool::scannerConnectionTask, this)); scanner.detach(); }按照代码的思路,我们取到connection之后什么时候释放应该是由客户端来决定,所以选择的是智能指针,但是当释放了之后,并不是delete,而是重新放回队列里面,所以要重写析构函数。
我们取connection的时候,有可能里面已经没有connection了,所以我们需要判断一下,不过不是没有就立马返回客户端,而是等待100ms(持有锁),这个中间如果有connection被释放了,那么就会立刻跳过if,执行while()循环,然后跳出。
std::shared_ptr<Litguidyo::Connection> Litguidyo::ConnectionPool::getConnection() { std::unique_lock<std::mutex> lock(_queueMutex); while (_connectionQueue.empty()) { // 让线程先等待100ms,如果100ms内没有连接,就继续执行代码 if (std::cv_status::timeout == cv.wait_for(lock, std::chrono::microseconds(_connectionTimeout))) { if (_connectionQueue.empty()) { LOG("ConnectionPool is empty, please check the configuration file! "); return nullptr; } } } std::shared_ptr<Connection> sp(_connectionQueue.front(), [&](Connection* p) { std::lock_guard<std::mutex> lock(_queueMutex); p->refreshAliveTime(); _connectionQueue.push(p); }); _connectionQueue.pop(); if (_connectionQueue.empty()) { cv.notify_all(); // 通知消费线程 } return sp; }只有队列是空的才会生产connection(不够用了),并且还不可以超过最大的值,当生产完了之火唤醒消费者。因为这里只有一个生产线程,所以完全不需要担心通知到生产线程,并且有while()来保证安全。
void Litguidyo::ConnectionPool::produceConnectionTask() { while(true) { std::unique_lock<std::mutex> lock(_queueMutex); while (!_connectionQueue.empty()) { cv.wait(lock); // 生产线程进行等待 } if (_currentConnectionSize < _maxConnectionSize) { Connection* connection = new Connection(); connection->refreshAliveTime(); // 刷新起始时间 connection->connect(_ip, _port, _username, _password, _dbname); _connectionQueue.push(connection); _currentConnectionSize++; } cv.notify_all(); // 通知消费线程 } }这个线程不是一直循环,而是隔一段时间扫描一次,如果发现队列里面的数量已经超过了初始化的数量,而且存活时间已经超过了限制(这个connection没有被长时间使用),就会删除。
void Litguidyo::ConnectionPool::scannerConnectionTask() { while (true) { std::this_thread::sleep_for(std::chrono::milliseconds(_maxIdleTime)); std::unique_lock<std::mutex> lock(_queueMutex); while (_currentConnectionSize > _initConnectionSize) { Connection* temp = _connectionQueue.front(); if (temp->getAliveTime() > std::chrono::milliseconds(_maxIdleTime)) { _connectionQueue.pop(); _currentConnectionSize--; delete temp; } else { break; // 队头都没有超时,那么之后的连接都不可能超时,不需要继续扫描 } } } }总结
本篇文章到这里就结束了!!!希望可以帮助大家理解~~~