MySQL数据库连接池

330 阅读8分钟

关键技术点

  • MySQL数据库编程
  • 单例模式
  • queue队列容器
  • C++11多线程编程、线程互斥、线程同步通信和
    unique_lock
  • 基于CAS的原子整形、智能指针shared_ptr
  • lambda表达式、生产者-消费者线程模型

项目背景

为了提高MySQL的访问瓶颈,除了在服务端缓存常用数据之外,还可以利用连接池来提高MySQL的访问效率。在高并发情况下,大量的TCP三次握手、MySQL Server连接认证、MySQL Server关闭连接回收资源和TCP四次挥手导致性能变差,增加连接池可以减少这部分的的性能损耗。

连接池主要功能点

连接池一般包含了数据库连接所用的ip、port、username、password、及其他性能参数,例如初始连接量、最大连接量、最大空闲时间、连接超时时间等,连接池主要就是实现上述四大通用基础功能。

  • 初始连接量(initSize): 表示连接池事先会和MySQL Server创建initSize个数的connection连接,当
    应用发起MySQL访问时,不用再创建和MySQL Server新的连接,直接从连接池中获取一个可用的连接
    就可以,使用完成后,并不去释放connection,而是把当前connection再归还到连接池当中。
  • 最大连接量(maxSize): 当并发访问MySQL Server的请求增多时,初始连接量已经不够使用了,此
    时会根据新的请求数量去创建更多的连接给应用去使用,但是新创建的连接数量上限是maxSize,不能
    无限制的创建连接,因为每一个连接都会占用一个socket资源,一般连接池和服务器程序是部署在一台
    主机上的,如果连接池占用过多的socket资源,那么服务器就不能接收太多的客户端请求了。当这些连
    接使用完成后,再次归还到连接池当中来维护。
  • 最大空闲时间(maxidleTime):当访问MySQL的并发请求多了以后,连接池里面的连接数量会动态
    增加,上限是maxSize个,当这些连接用完再次归还到连接池当中。如果在指定的maxIdleTime里面,
    这些新增加的连接都没有被再次使用过,那么新增加的这些连接资源就要被回收掉,只需要保持初始连
    接量initSize个连接就可以了。
  • 连接超时时间(connectionTimeout):当访问MySQL的并发请求多了以后,连接池里面的连接数量会动态
    增加,上限是maxSize个,当这些连接用完再次归还到连接池当中。如果在指定的maxIdleTime里面,
    这些新增加的连接都没有被再次使用过,那么新增加的这些连接资源就要被回收掉,只需要保持初始连
    接量initSize个连接就可以了。

功能实现设计

Connection类封装连接池

Connection类封装对数据库的操作

连接池主要包含了以下功能点:
1.连接池只需要一个实例,所以ConnectionPool以单例模式进行设计
2.从ConnectionPool中可以获取和MySQL的连接Connection
3.空闲连接Connection全部维护在一个线程安全的Connection队列中,使用线程互斥锁保证队列的线
程安全
4.如果Connection队列为空,还需要再获取连接,此时需要动态创建连接,上限数量是maxSize
5.队列中空闲连接时间超过maxIdleTime的就要被释放掉,只保留初始的initSize个连接就可以了,这个
功能点肯定需要放在独立的线程中去做
6.如果Connection队列为空,而此时连接的数量已达上限maxSize,那么等待connectionTimeout时间
如果还获取不到空闲的连接,那么获取连接失败,此处从Connection队列获取空闲连接,可以使用带
超时时间的mutex互斥锁来实现连接超时时间
7.用户获取的连接用shared_ptr智能指针来管理,用lambda表达式定制连接释放的功能(不真正释放
连接,而是把连接归还到连接池中)
8.连接的生产和连接的消费采用生产者-消费者线程模型来设计,使用了线程间的同步通信机制条件变量
和互斥锁

核心代码

公共类public

#pragma once

// 日志函数
#define Log(str) \
        std::cout << __FILE__ << ":" << __LINE__ << " " << \
        __TIMESTAMP__ << " : " << str << std::endl;

Connection类

自行拆分为头文件和源文件

/*
    实现MySQL数据库操作
*/
#include <mysql.h>
#include <public.h>
#include <string>
using namespace std;
class Connection
{
public:
    Connection(){
        _conn = mysql_init(nullptr);
    }
    ~Connection(){
        if (_conn){
            mysql_close(_conn);
        }
    }
    // 连接数据库
    bool connect(string ip, unsigned int port, string user, string password, string dbname){
        MYSQL *p = mysql_real_connect(_conn, ip.c_str(), user.c_str(), password.c_str(), dbname.c_str(), port, nullptr, 0);
        return p;
    }
    // 更新(insert update delete)
    bool update(string sql) {
        if (mysql_query(_conn, sql.c_str())) {
            LOG("更新失败"+sql);
            return false;
        }
        return true;
    }

    MYSQL_RES* query(string sql) {
         if (mysql_query(_conn, sql.c_str())) {
            LOG("查询失败"+sql);
            return nullptr;
        }
        return mysql_use_result(_conn);
    }

    void printQueryResult(MYSQL_RES* result) {
    if (!result) {
        std::cerr << "结果集为空" << std::endl;
        return;
    }

         MYSQL_ROW row;
    unsigned int num_fields = mysql_num_fields(result);
    MYSQL_FIELD* field = mysql_fetch_fields(result);
        // 打印表头
    for (unsigned int i = 0; i < num_fields; i++) {
        std::cout << fields[i].name << "\t";
    }
     std::cout << std::endl;
         // 打印每一行的数据
    while ((row = mysql_fetch_row(result))) {
        for (unsigned int i = 0; i < num_fields; i++) {
            std::cout <<(row[i] ? row[i] : "NULL") << "\t";
        }
         std::cout << std::endl;
    }
        // 释放结果集
    mysql_free_result(result);
    }
private:
    MYSQL *_conn; //标识一条和Server的连接
}

ConnectionPool类

1. 首先创建一个单例模式的类

class ConnectionPool {
public:
    static ConnectionPool* getConnectionPool();
private:
    ConnectionPool();
    ~ConnectionPool();
    
	string _ip; // ip地址
    unsigned int _port; // 端口
    string _username;
    string _password;
    string _dbname;
    int _initSize; // 初始连接数
    int _maxSize;  // 最大连接数
    int _maxIdleTime;  // 最大空闲时间
    int _connectionTimeout; // 超时时间
}
ConnectionPool *ConnectionPool::getConnectionPool() {
    static ConnectionPool pool;
    return &pool;
}

2.解析配置文件

# mysql配置文件
ip=127.0.0.1
port=3306
username=root
password=1
dbname=chat
initSize=10
maxSize=1024
#最大空闲时间单位是s
maxIdleTime=60
#连接超时时间单位ms
maxConnectionTimeout=100
bool ConnectionPool::loadConfigFile(const string& filename){
    ifstream infile(filename.c_str());
    if (!infile) {
        LOG(filename + "is not exist!");
        return false;
    }
    string line;
    while(getline(infile, line)) {
        if(line.empty()) {
            continue;
        }
        size_t pos = line.find('=');
        if (pos == string::npos) { // 无效配置
            continue;
        }
        string key = line.substr(0, pos);
        string value = line.substr(pos+1);
        if (key == "ip") {
            _ip = value;
        } else if (key == "port"){
            _port = stoul(value);
        } else if (key == "username") {
            _username = value;
        } else if (key == "password") {
            _password = value;
        } else if (key == "dbname") {
            _dbname = value;
        } else if (key == "initSize") {
            _initSize = stoi(value);
        } else if (key == "maxsize") {
            _maxSize = stoi(value);
        } else if (key == "maxIdleTime") {
            _maxIdleTime = stoi(value);
        } else if (key == "maxConnectionTimeout") {
            _connectionTimeout = stoi(value);
        }
    }
    return true;
}

3. 加载配置文件并且进行初始化

在构造函数中进行

ConnectionPool::ConnectionPool() {
    // 加载配置项
    if (!loadConfigFile("config/mysql.ini")) {
        return ;
    }

    // 创建初始数量链接
    for (int i = 0; i < _initSize; ++i) {
        Connection *p = new Connection();
        p->connect(_ip, _port, _username, _password, _dbname);
        _connectionQue.push(p);
        ++_connectionCnt;
    }
}

4. 设置一个子线程来监控这些连接

连接不够就增加,多了就减少

ConnectionPool::ConnectionPool() {
    .....

    // 启动一个新的线程,作为连接的生产者
    //thread produce(&ConnectionPool::produceConnectionTask, this);
    thread produce(std::bind(&ConnectionPool::produceConnectionTask, this));
}
void ConnectionPool::produceConnectionTask() {
    for (;;) {
        // 操作队列需要加锁
        unique_lock<mutex> lock(_queueMutex);
        while (!_connectionQue.empty()){
            // 暂时解锁由 lock 持有的互斥锁。
            //将当前线程置为等待状态,直到其他线程调用 _cv.notify_one() 或 _cv.notify_all() 来唤醒等待中的一个或所有线程。
            //当线程被唤醒后,wait 会重新获取互斥锁并继续执行。
            _cv.wait(lock); //队列不空,生产线程处于等待状态
        }

        // 连接未达上限,继续创建
        if (_connectionCnt < _maxSize) {
            Connection *p = new Connection();
            p->connect(_ip, _port, _username, _password, _dbname);
            _connectionQue.push(p);
            ++_connectionCnt;
        }

        // 通知消费者线程消费
        _cv.notify_one();
    }
}

5. 设置消费者

shared_ptr<Connection> ConnectionPool::getConnection() {
    unique_lock<mutex> lock(_queueMutex);
    if (_connectionQue.empty()) {
        _cv.wait_for(lock, chrono::milliseconds(_connectionTimeout));
        if (_connectionQue.empty()) {
            LOG("获取空闲连接超时");
            return nullptr;
        }
    }

    shared_ptr<Connection> sp(_connectionQue.front());
    _connectionQue.pop();
    if (_connectionQue.empty()) {
        // 消费完发现空了,及时通知生产
        // 谁消费了最后一个,谁最进行通知
        // 不加也可以,因为生产者会判断队列是否空了,进而跳过wait等待
        _cv.notify_all();
    }
    return sp;
}

6. 重定义消费者使用完之后的析构

智能指针出作用域自动释放,默认会delete,我们需要重写让他放回池子, 直接给构造传第二个参数

shared_ptr<Connection> ConnectionPool::getConnection() {
    ...

    shared_ptr<Connection> sp(_connectionQue.front(), [&](Connection *pCon){
        unique_lock<mutex> lock(_queueMutex);
        _connectionQue.push(pCon);
    });
    ...
}

7. 空闲时间处理

将连接放入连接池和归还时都要进行刷新

新扫描建线程

void ConnectionPool::scannerConnectionTask() {
    for (;;) {
        this_thread::sleep_for(chrono::seconds(_maxIdleTime));
        // 扫描队列释放多余连接
        unique_lock<mutex> lock(_queueMutex);
        while (_connectionCnt > _initSize) {
            // 队头空闲时间小于60,后边也不用看了
            Connection *p = _connectionQue.front();
            if (p->getAliveTime() >= (_maxIdleTime * 1000)) {
                _connectionQue.pop();
                delete p;
            } else {
                break;
            }
        }
    }
} 

测试

建表

create DATABASE chat;

USE chat;

CREATE TABLE user (
    id INT AUTO_INCREMENT PRIMARY KEY,  -- 添加一个自增的主键
    name VARCHAR(50) NOT NULL,
    age TINYINT UNSIGNED NOT NULL CHECK (age >= 0 AND age <= 120),  -- 确保年龄合理
    sex ENUM('male', 'female') DEFAULT 'male'  -- 设置默认值
);
#include <iostream>
#include <sstream>
#include <thread>
#include <ctime>
#include "./include/ConnectionPool.h"

int main() {
    clock_t begin = clock();


//    for (int i = 0; i < 1000; i++) {
        // 单线程未使用线程池,2708ms
//        Connection conn;
//        const char *sql = "INSERT INTO `user` (`name`, `age`, `sex`) VALUES ('zhang san', 20, 'male')";
//        if (conn.connect("127.0.0.1", 3306, "root", "123456", "chat") == false) {
//            LOG("连接失败");
//        }
//        conn.update(sql);

        //单线程使用线程池 579ms
//        ConnectionPool *cp = ConnectionPool::getConnectionPool();
//        shared_ptr<Connection> sp = cp->getConnection();
//        string sql = "INSERT INTO `user` (`name`, `age`, `sex`) VALUES ('zhang san', 20, 'male')";
//		sp->update(sql);
//    }

    
    // 三线程 942ms
    thread t1([]() {
        ConnectionPool *cp = ConnectionPool::getConnectionPool();
        for (int i = 0; i < 1000; i++) {
            shared_ptr<Connection> sp = cp->getConnection();
            string sql = "INSERT INTO `user` (`name`, `age`, `sex`) VALUES ('zhang san', 20, 'male')";
            sp->update(sql);
        }
    });


    thread t2([]() {
        ConnectionPool *cp = ConnectionPool::getConnectionPool();
            for (int i = 0; i < 1000; i++) {
                shared_ptr<Connection> sp = cp->getConnection();
                string sql = "INSERT INTO `user` (`name`, `age`, `sex`) VALUES ('zhang san', 20, 'male')";
                sp->update(sql);
            }
    });


    thread t3([]() {
        ConnectionPool *cp = ConnectionPool::getConnectionPool();
        for (int i = 0; i < 1000; i++) {
            shared_ptr<Connection> sp = cp->getConnection();
            string sql = "INSERT INTO `user` (`name`, `age`, `sex`) VALUES ('zhang san', 20, 'male')";
            sp->update(sql);
        }
    });
    t1.join();
    t2.join();
    t3.join();

    clock_t end = clock();
    cout << (end - begin) << "ms" << endl;
    return 0;
}

项目地址

CommonConnectionPool: 数据库连接池项目