高并发服务器数据库连接池设计详解
前置知识:MySQL C API 解析
代码中所有 mysql_xxx 开头的函数,都来自 MySQL 官方提供的 C 语言客户端开发库 libmysqlclient,是 C/C++ 操作 MySQL 的原生标准接口。我们需要先把代码中出现的MYSQL函数搞明白。
1. MYSQL 指针到底是什么?
MYSQL 是 MySQL 客户端库定义的连接句柄结构体,你可以把它理解成「一条数据库连接的总控遥控器」:
-
它内部封装了:网络套接字、连接状态、字符集配置、错误信息、接收缓冲区、事务状态等所有和这条连接相关的上下文数据;
-
我们不能手动 malloc/new 这个结构体,必须通过官方函数创建、使用、销毁;
-
后续所有数据库操作(增删改查、事务、预处理等)都必须通过这个指针执行。
2. 代码中出现的 MySQL 函数逐一说
(1) mysql_init —— 创建并初始化连接句柄
MYSQL* mysql_init(MYSQL* mysql);
-
核心作用:在堆内存上分配一个
MYSQL结构体,并完成内部字段的默认初始化,为后续建立真实连接做准备。 -
参数说明:
-
传入
nullptr:函数会自动分配一块全新的内存,初始化后返回指针; -
传入已有的
MYSQL*:函数会重置这个结构体的状态(复用已有内存)。
-
-
返回值:成功返回初始化后的
MYSQL*指针;内存不足时返回nullptr。 -
对应代码:
MYSQL *sql = nullptr; sql = mysql_init(sql); // 传入nullptr,让库帮我们创建并初始化一个空连接句柄执行完这一步,我们只是拿到了一个「空壳句柄」—— 结构体有了,但还没真正和 MySQL 服务器建立网络连接。
(2) mysql_real_connect —— 真正建立数据库连接
MYSQL* mysql_real_connect(
MYSQL *mysql, // 【入】mysql_init 初始化好的句柄
const char *host, // 【入】数据库主机地址(IP/域名)
const char *user, // 【入】数据库用户名
const char *passwd, // 【入】数据库密码
const char *db, // 【入】要默认使用的数据库名
unsigned int port, // 【入】数据库端口(MySQL 默认 3306)
const char *unix_socket,// 【入】Unix 域套接字,传 nullptr 表示用 TCP/IP 连接
unsigned long client_flag // 【入】连接选项标志位,0 为默认配置
);
-
核心作用:拿着初始化好的句柄,通过 TCP 网络和 MySQL 服务器完成「三次握手 → 账号认证 → 权限校验 → 数据库选择 → 字符集协商」的完整连接流程。
-
返回值:
-
连接成功:返回和第一个参数相同的
MYSQL*指针(此时这个指针就代表一条可用的真实连接); -
连接失败:返回
nullptr(常见原因:地址 / 端口错误、账号密码不对、数据库不存在、网络不通)。
-
-
对应代码:
sql = mysql_real_connect(执行完这一步且非空,这条连接就可以直接用来执行 SQL 语句了。
(3) mysql_close —— 关闭连接、释放句柄内存
void mysql_close(MYSQL* mysql
-
核心作用:断开和服务器的网络连接,同时释放
mysql_init分配的结构体内存。 -
注意事项:一个连接只能关闭一次,重复关闭会导致内存错误;关闭后指针立即失效,不能再执行任何操作。
-
对应代码:
ClosePool中循环调用,逐个销毁所有连接。
(4) mysql_library_end —— 释放 MySQL 库全局资源
void mysql_library_end(void);
-
核心作用:整个程序退出前调用一次,释放 MySQL 客户端库本身占用的全局资源(比如字符集缓存、全局内存池、线程局部存储等)。
-
补充:对应的初始化函数是
mysql_library_init,但代码里没显式写 —— 因为mysql_init会自动触发全局库的初始化,只要保证程序结束时调用一次 end 即可。
SqlConnPool连接池解析
为什么需要连接池?
如果没有连接池,每次执行 SQL 都要走「新建连接 → 执行 SQL → 关闭连接」的流程,会有严重的性能问题:
-
性能开销大:每次连接都要 TCP 三次握手、MySQL 认证、权限校验,耗时几毫秒到几十毫秒,高并发下会成为性能瓶颈;
-
连接数失控:如果同时有几百上千个线程访问数据库,会创建几百上千条连接,MySQL 服务器承受不住(连接数过多会耗尽数据库内存和 CPU)。
连接池的核心思想就是 「空间换时间,复用连接,限流管控」:
-
程序启动时提前创建好 N 条数据库连接,统一放在池子里管理;
-
业务线程要用的时候从池子里取,用完还回去,不用每次都创建销毁;
-
最多只有 N 条连接,不会把数据库打挂。
成员变量
我们先来看连接池中的私有成员变量
// ==================== 成员变量 ====================
int MAX_CONN_; //最大连接数
int userCount_; //正在使用的连接数
int freeCount_; //空闲连接数
std::queue<MYSQL *> connQue_; //连接队列
std::mutex mtx_; //互斥锁
sem_t semId_; //信号量(控制链接数量)
connQue_存放所有当前可用、没人在用的数据库连接。
mtx_配合lock_guard保证多线程安全。
semId_信号量做资源计数+阻塞等待,其值=当前空闲连接的数量
构造与析构
构造函数、析构函数都是 private 私有:禁止外部直接创建 / 销毁对象,配合单例模式使用。
/**
* @brief 构造函数
*
* 初始化成员变量
*/
SqlConnPool::SqlConnPool()
{
//正在使用和空闲链接都初始化为0
userCount_ = 0;
freeCount_ = 0;
}
/**
* @brief 析构函数
*
* 关闭连接池
*/
SqlConnPool::~SqlConnPool()
{
ClosePool();
}
这里构造函数只做简单的计数初始化,真正的连接初始化在Init函数内(因为需要外部传入数据库参数,不能在构造时就创建)
析构函数自动调用ClosePool()关闭所有连接。
单例模式:Instance()
/**
* @brief 获取连接池单例
*
* 使用 static 局部变量实现线程安全的单例
* C++11 保证 static 局部变量的初始化是线程安全的
*
* @return SqlConnPool* 连接池指针
*/
SqlConnPool *SqlConnPool::Instance()
{
static SqlConnPool connPool; //只能被初始化一次,所以线程安全
return &connPool; //返回引用
}
连接池是全局资源,整个程序只需要一个实例:
-
如果创建多个连接池,每个池都创建一堆连接,会浪费数据库连接资源,也违背了限流的初衷;
-
单例模式保证全局唯一,统一管理所有连接。
实现原理
用 C++11 静态局部变量 实现,业内叫「Magic Static」:
-
static局部变量只会在第一次调用时初始化一次,后续调用都返回同一个对象; -
C++11 标准明确规定:静态局部变量的初始化是线程安全的,多个线程同时第一次调用也不会出现重复初始化的问题。
这种实现是现代 C++ 单例的最优写法:不用手动加锁,代码简洁,天然线程安全,比「懒汉式加锁」「饿汉式」都更优雅。
连接池管理
初始化函数
/**
* @brief 初始化连接池
*
* 创建指定数量的 MySQL 连接
*
* 初始化流程:
* 1. mysql_init 初始化 MySQL 对象
* 2. mysql_real_connect 连接到数据库
* 3. 将连接放入队列
* 4. 初始化信号量
*
* @param host 数据库主机地址
* @param port 数据库端口
* @param user 用户名
* @param pwd 密码
* @param dbName 数据库名称
* @param connSize 连接池大小
*/
void SqlConnPool::Init(const char *host, int port, const char *user, const char *pwd, const char *dbName, int connSize = 10)
{
assert(connSize > 0);
for (int i = 0; i < connSize; i++)
{
MYSQL *sql = nullptr;
//初始化MySQL对象
sql = mysql_init(sql);
if (!sql)
{
LOG_ERROR("MySql init error!");
assert(sql);
}
//链接到数据库
sql = mysql_real_connect(sql, host, user, pwd, dbName, port, nullptr, 0);
if (!sql)
{
LOG_ERROR("MySql Connect error!");
}
//将链接放入队列中
connQue_.push(sql);
}
MAX_CONN_ = connSize;
//初始化信号量,初始值为连接池大小
//信号量用于控制通知获取链接的线程数量
sem_init(&semId_, 0, MAX_CONN_);
}
循环connSize次,每次都进行mysql_init(初始化MySQL对象),mysql_real_connect(真正连接到数据库),通过connQue_push放入空闲队列中的流程。最后进行信号量初始化。
信号量初始化:
sem_init(&semId_, 0, MAX_CONN_) 三个参数:
-
第一个:信号量变量的地址;
-
第二个:
0表示信号量用于同一进程内的线程间同步;非 0 表示进程间共享; -
第三个:信号量的初始值,这里设为最大连接数 —— 代表一开始有
MAX_CONN_个可用连接资源。
获取连接
/**
* @brief 获取一个数据库连接
*
* 获取流程:
* 1. 检查连接池是否为空
* 2. 等待信号量(P 操作)
* 3. 加锁,从队列取出连接
* 4. 解锁,返回连接
*
* @return MYSQL* MySQL 连接指针,nullptr 表示失败
*/
MYSQL *SqlConnPool::GetConn()
{
MYSQL *sql = nullptr;
//检查连接池
if (connQue_.empty())
{
LOG_WARN("SqlConnPool busy!");
return nullptr;
}
//等待信号量(P操作)
//如果信号量为0,阻塞直到有链接可用
sem_wait(&semId_);
{
lock_guard<mutex> locker(mtx_);
sql = connQue_.front();
connQue_.pop();
}
return sql;
}
这是「信号量 + 互斥锁」的经典并发组合,两者分工明确:
-
信号量
sem_wait:负责资源计数 + 阻塞等待。每调用一次,信号量的值减 1;如果信号量已经是 0,说明所有连接都被占用了,线程会阻塞在这里挂起,直到有其他线程归还连接、信号量大于 0。 -
互斥锁
lock_guard:负责保护临界资源(队列)。STL 的队列不是线程安全的,多个线程同时执行pop()会出现竞态条件(比如两个线程都取到同一个连接、队列越界等),所以必须加锁。
归还连接
void SqlConnPool::FreeConn(MYSQL* sql){
assert(sql);
lock_guard<mutex> locker(mtx_);
connQue_.push(sql); // 连接放回队列尾部
sem_post(&semId_); // V操作:信号量+1
}
先对其进行校验防止归还空指针,然后加锁放回队尾,sem_post 执行 V 操作,信号量的值加 1。如果此时有线程正阻塞在 sem_wait 上,会被唤醒,去取刚归还的连接。
关闭连接池
/**
* @brief 关闭连接池
*
* 关闭所有 MySQL 连接,清理资源
*/
void SqlConnPool::ClosePool(){
lock_guard<mutex> locker(mtx_);
while(!connQue_.empty()){
auto item = connQue_.front();
connQue_.pop();
//关闭连接并释放内存
mysql_close(item);
}
//释放 MySQL 客户端库本身占用的全局资源
mysql_library_end();
}
关闭过程全程加锁,防止其他线程同时取/放连接导致野指针。逐个对队列中的连接调用mysql_close关闭连接,最后使用mysql_library_end做资源收尾。
获取空闲连接数
/**
* @brief 获取空闲连接数量
*
* @return int 空闲连接数
*/
int SqlConnPool::GetFreeConnCount(){
lock_guard<mutex> locker(mtx_);
return connQue_.size();
}
加锁获取空闲队列大小
RAII 封装类 SqlConnRAII
为什么需要这个类?
如果只靠手动调用 GetConn() 和 FreeConn(),会有两个严重问题:
-
容易忘记归还:业务代码复杂,分支多,很容易漏掉
FreeConn,导致连接永远被占用,池子里的连接越来越少,最终耗尽(连接泄漏); -
异常不安全:如果获取连接后、归还前,代码抛出了异常,后面的
FreeConn就永远执行不到了,同样会泄漏。
所以我们用 RAII 机制 来封装连接的生命周期,让编译器帮我们自动管理资源。
RAII 是 C++ 的核心编程思想,全称是 Resource Acquisition Is Initialization(资源获取即初始化):
-
利用对象的构造函数获取资源;
-
利用对象的析构函数释放资源;
-
C++ 保证:对象离开作用域时,一定会执行析构函数,无论正常返回还是异常退出。
成员变量
MYSQL *sql_; /**< MySQL 连接指针 */
SqlConnPool* connpool_; /**< 连接池指针 */
两个私有指针成员分别管理MySQL连接指针和数据库指针。
构造与析构
/**
* @brief 构造函数
*
* 从连接池获取数据库连接
*
* @param sql 指向 MYSQL 指针的指针(用于返回连接)
* @param connpool 连接池指针
*/
SqlConnRAII(MYSQL** sql, SqlConnPool *connpool) {
assert(connpool);
*sql = connpool->GetConn(); // 从连接池获取连接
sql_ = *sql;
connpool_ = connpool;
}
/**
* @brief 析构函数
*
* 将数据库连接释放回连接池
*/
~SqlConnRAII() {
if(sql_) { connpool_->FreeConn(sql_); }
}
为什么传二级指针 MYSQL\** sql?
我们的目的是:把获取到的连接指针,赋值给外部的 MYSQL* 变量。
C++ 中函数参数默认是值传递,如果传一级指针 MYSQL* sql,函数里修改的只是形参的副本,外部的实参不会变。
传二级指针(指针的地址),我们就能通过解引用 *sql,修改外部的原始指针变量。
也可以用「指针的引用」MYSQL*& sql 实现,效果一样,写法更简洁。
完整代码
sqlconnpool.h
/**
* @file sqlconnpool.h
* @brief 数据库连接池类头文件
*
* SqlConnPool 实现了 MySQL 数据库连接池,用于:
* - 复用数据库连接,避免频繁创建/销毁
* - 控制并发连接数,防止数据库过载
*
* 设计特点:
* - 单例模式,全局唯一实例
* - 使用信号量控制连接数量
* - 使用 mutex 保护连接队列
*
* 线程安全:
* - GetConn/FreeConn 使用信号量 + 互斥锁
*/
#ifndef SQLCONNPOOL_H
#define SQLCONNPOOL_H
#include <mysql/mysql.h>
#include <string>
#include <queue>
#include <mutex>
#include <semaphore.h>
#include <thread>
#include <assert.h>
#include "../log/log.h"
/**
* @class SqlConnPool
* @brief 数据库连接池类(单例模式)
*
* 管理 MySQL 连接的创建、获取和释放
*/
class SqlConnPool
{
public:
/**
* @brief 获取连接池单例
*
* 使用 static 局部变量实现线程安全的单例
*
* @return SqlConnPool* 连接池指针
*/
static SqlConnPool *Instance();
/**
* @brief 获取一个数据库连接
*
* 如果连接池为空,返回 nullptr
*
* @return MYSQL* MySQL 连接指针,nullptr 表示失败
*/
MYSQL *GetConn();
/**
* @brief 释放一个数据库连接
*
* 将连接返回到连接池
*
* @param conn MySQL 连接指针
*/
void FreeConn(MYSQL *conn);
/**
* @brief 获取空闲连接数量
*
* @return int 空闲连接数
*/
int GetFreeConnCount();
/**
* @brief 初始化连接池
*
* 创建指定数量的 MySQL 连接
*
* @param host 数据库主机地址
* @param port 数据库端口
* @param user 用户名
* @param pwd 密码
* @param dbName 数据库名称
* @param connSize 连接池大小
*/
void Init(const char *host, int port, const char *user, const char *pwd, const char *dbName, int connSize);
//关闭连接池,关闭所有的MySQL连接
void ClosePool();
private:
/**
* @brief 构造函数(私有)
*
* 防止外部创建实例
*/
SqlConnPool();
/**
* @brief 析构函数
*
* 关闭连接池
*/
~SqlConnPool();
// ==================== 成员变量 ====================
int MAX_CONN_; //最大连接数
int userCount_; //正在使用的连接数
int freeCount_; //空闲连接数
std::queue<MYSQL *> connQue_; //连接队列
std::mutex mtx_; //互斥锁
sem_t semId_; //信号量(控制链接数量)
};
#endif
sqlconnpool.cpp
/**
* @file sqlconnpool.cpp
* @brief 数据库连接池类实现文件
*
* 实现了 MySQL 数据库连接池的创建、获取和释放
*/
#include "sqlconnpool.h"
using namespace std;
// ==================== 构造与析构 ====================
/**
* @brief 构造函数
*
* 初始化成员变量
*/
SqlConnPool::SqlConnPool()
{
//正在使用和空闲链接都初始化为0
userCount_ = 0;
freeCount_ = 0;
}
/**
* @brief 析构函数
*
* 关闭连接池
*/
SqlConnPool::~SqlConnPool()
{
ClosePool();
}
// ==================== 单例模式 ====================
/**
* @brief 获取连接池单例
*
* 使用 static 局部变量实现线程安全的单例
* C++11 保证 static 局部变量的初始化是线程安全的
*
* @return SqlConnPool* 连接池指针
*/
SqlConnPool *SqlConnPool::Instance()
{
static SqlConnPool connPool; //只能被初始化一次,所以线程安全
return &connPool; //返回引用
}
// ==================== 连接池管理 ====================
/**
* @brief 初始化连接池
*
* 创建指定数量的 MySQL 连接
*
* 初始化流程:
* 1. mysql_init 初始化 MySQL 对象
* 2. mysql_real_connect 连接到数据库
* 3. 将连接放入队列
* 4. 初始化信号量
*
* @param host 数据库主机地址
* @param port 数据库端口
* @param user 用户名
* @param pwd 密码
* @param dbName 数据库名称
* @param connSize 连接池大小
*/
void SqlConnPool::Init(const char *host, int port, const char *user, const char *pwd, const char *dbName, int connSize = 10)
{
assert(connSize > 0);
for (int i = 0; i < connSize; i++)
{
MYSQL *sql = nullptr;
//初始化MySQL对象
sql = mysql_init(sql);
if (!sql)
{
LOG_ERROR("MySql init error!");
assert(sql);
}
//链接到数据库
sql = mysql_real_connect(sql, host, user, pwd, dbName, port, nullptr, 0);
if (!sql)
{
LOG_ERROR("MySql Connect error!");
}
//将链接放入队列中
connQue_.push(sql);
}
MAX_CONN_ = connSize;
//初始化信号量,初始值为连接池大小
//信号量用于控制通知获取链接的线程数量
sem_init(&semId_, 0, MAX_CONN_);
}
/**
* @brief 获取一个数据库连接
*
* 获取流程:
* 1. 检查连接池是否为空
* 2. 等待信号量(P 操作)
* 3. 加锁,从队列取出连接
* 4. 解锁,返回连接
*
* @return MYSQL* MySQL 连接指针,nullptr 表示失败
*/
MYSQL *SqlConnPool::GetConn()
{
MYSQL *sql = nullptr;
//检查连接池
if (connQue_.empty())
{
LOG_WARN("SqlConnPool busy!");
return nullptr;
}
//等待信号量(P操作)
//如果信号量为0,阻塞直到有链接可用
sem_wait(&semId_);
{
lock_guard<mutex> locker(mtx_);
sql = connQue_.front();
connQue_.pop();
}
return sql;
}
/**
* @brief 释放一个数据库连接
*
* 将连接返回到连接池
*
* 释放流程:
* 1. 加锁,将连接放入队列
* 2. 解锁
* 3. 释放信号量(V 操作)
*
* @param conn MySQL 连接指针
*/
void SqlConnPool::FreeConn(MYSQL*sql){
assert(sql);
lock_guard<mutex> locker(mtx_);
connQue_.push(sql);
//释放信号量(V操作)
//通知等待的线程有链接可用
sem_post(&semId_);
}
/**
* @brief 关闭连接池
*
* 关闭所有 MySQL 连接,清理资源
*/
void SqlConnPool::ClosePool(){
lock_guard<mutex> locker(mtx_);
while(!connQue_.empty()){
auto item = connQue_.front();
connQue_.pop();
//关闭连接并释放内存
mysql_close(item);
}
//释放 MySQL 客户端库本身占用的全局资源
mysql_library_end();
}
/**
* @brief 获取空闲连接数量
*
* @return int 空闲连接数
*/
int SqlConnPool::GetFreeConnCount(){
lock_guard<mutex> locker(mtx_);
return connQue_.size();
}
sqlconnRAII.h
/**
* @file sqlconnRAII.h
* @brief 数据库连接 RAII 封装头文件
*
* SqlConnRAII 使用 RAII(Resource Acquisition Is Initialization)模式:
* - 构造函数中获取数据库连接
* - 析构函数中释放数据库连接
*
* 优点:
* - 自动管理资源,避免内存泄漏
* - 异常安全,即使发生异常也会释放资源
* - 代码简洁,无需手动释放
*
* 使用示例:
* @code
* MYSQL* sql;
* SqlConnRAII(&sql, SqlConnPool::Instance());
* // 使用 sql 进行数据库操作...
* // 无需手动释放,析构函数会自动释放
* @endcode
*/
#ifndef SQLCONNRAII_H
#define SQLCONNRAII_H
#include "sqlconnpool.h"
/**
* @class SqlConnRAII
* @brief 数据库连接 RAII 封装类
*
* 自动管理数据库连接的获取和释放
*/
class SqlConnRAII
{
public:
/**
* @brief 构造函数
*
* 从连接池获取数据库连接
*
* @param sql 指向 MYSQL 指针的指针(用于返回连接)
* @param connpool 连接池指针
*/
SqlConnRAII(MYSQL**sql,SqlConnPool*connpool){
assert(connpool);
*sql = connpool->GetConn();//从连接池获取链接
sql_ =*sql;
connpool_ =connpool;
}
/**
* @brief 析构函数
*
* 将数据库连接释放回连接池
*/
~SqlConnRAII(){
if(sql_){
connpool_->FreeConn(sql_);
}
}
private:
MYSQL*sql_;//mysql链接指针
SqlConnPool* connpool_;//连接池指针
};
#endif
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)