muduo网络库(十六):新增连接池模块
- muduo网络库(十六):新增连接池模块
-
- 为什么需要连接池
- 用餐厅来理解
- 整体架构
- 单例模式:全局唯一连接池
- [初始化:预创建 + 启动回收](#初始化:预创建 + 启动回收)
- [创建连接:MySQL C API](#创建连接:MySQL C API)
- getConnection:核心获取逻辑
- [RAII 自动归还:自定义删除器的精髓](#RAII 自动归还:自定义删除器的精髓)
- 回收线程:后台清理闲置连接
- 销毁:优雅退出
- 线程安全分析
- 完整生命周期总览
- 设计价值总结
- 系列串联
muduo网络库(十六):新增连接池模块
为什么需要连接池
muduo 是 Reactor 模式网络库,擅长处理高并发连接。但如果每条消息都要创建销毁数据库连接,网络层再高效也被数据库层拖死------木桶效应。
无连接池时:
muduo 网络层: 每秒数万事件(epoll)
↓
数据库层: 每秒几十次(建连开销 ~30ms/次)
↓
整体吞吐被数据库层卡死
有连接池后:
muduo 网络层: 每秒数万事件
↓
连接池: minConn=10 个连接随时待命
↓
整体吞吐: 瓶颈转移到 MySQL 自身性能
一次 MySQL 建连需要 TCP 三次握手 + 认证 + 权限检查,耗时约 30ms。而一条 SQL 执行可能只需 1ms。如果没有连接池,99% 的时间浪费在建连上。
连接池的核心思路是用空间换时间------用少量常驻资源(连接占用的内存)换取每次操作省去的建连时间。让数据库操作的成本从"次次建连"降为"队列的 push/pop",从而匹配网络层的高并发能力。
用餐厅来理解
把数据库想象成后厨,连接池就是备餐台上的托盘:
| 连接池概念 | 餐厅类比 |
|---|---|
| 连接池 | 备餐台 |
| MYSQL 连接 | 托盘 |
| getConnection() | 拿一个托盘 |
| 归还连接 | 用完放回备餐台 |
| minConn | 至少备 5 个托盘 |
| maxConn | 最多 20 个托盘 |
| idleTimeout | 闲置超 60 秒的多余托盘收走 |
| recycler 线程 | 餐厅后勤定期收多余托盘 |
服务员(业务线程)要用托盘时,从备餐台上直接拿------不用每次去仓库领新的。用完放回,下一个人继续用。托盘不够了临时去领(扩容),太多了闲置太久就收回(回收)。
整体架构
┌──────────────────────────────────────────────────────────────┐
│ ConnectionPool (单例) │
│ │
│ ┌──────────────────┐ ┌────────────────────────┐ │
│ │ connQueue_ │ │ getConnection() │ │
│ │ (空闲连接队列) │ ◀────── │ 返回 shared_ptr<MYSQL> │ │
│ │ │ │ │ │
│ │ [Conn] ← 最早 │ │ 自定义 deleter: │ │
│ │ [Conn] │ ──────► │ connQueue_.push(c) │ │
│ │ [Conn] ← 最新 │ │ cond_.notify_one()│ │
│ └────────┬─────────┘ └────────────────────────┘ │
│ │ │
│ │ recycler 线程 (后台) │
│ ▼ │
│ ┌──────────────────┐ │
│ │ 回收 idle > │ 总连接: totalConn_ │
│ │ idleTimeout │ 下限: minConn_ │
│ │ 的多余连接 │ 上限: maxConn_ │
│ └──────────────────┘ │
└──────────────────────────────────────────────────────────────┘
连接池内部维护一个 FIFO 队列 connQueue_,存放空闲连接。核心参数四个:
| 参数 | 含义 | 作用 |
|---|---|---|
minConn_ |
最小连接数 | 池中始终保持的连接数,保证冷启动也能立即响应 |
maxConn_ |
最大连接数 | 防止突发流量耗尽数据库连接 |
idleTimeout |
空闲超时 | 闲置超过此时间的多余连接被回收 |
connTimeout |
获取超时 | 等不到连接时的最长等待时间 |
单例模式:全局唯一连接池
cpp
ConnectionPool* ConnectionPool::getInstance() {
static ConnectionPool pool; // C++11 保证线程安全的静态局部变量
return &pool;
}
这里利用了 C++11 的 Magic Static 特性:静态局部变量的初始化在 C++11 标准中保证是线程安全的------如果多个线程同时首次执行到这行,编译器保证只有一个线程执行初始化,其余线程阻塞等待。
配合两个设计保证单例的严密性:
- 构造函数
private,外部无法new或创建栈对象 - 继承
noncopyable,禁止拷贝和赋值
初始化:预创建 + 启动回收
cpp
void init(string ip, uint16_t port, string user, string pwd,
string db, int minConn, int maxConn,
int idleTimeout, int connTimeout) {
// 1. 预创建 minConn 个连接
for (int i = 0; i < minConn_; ++i) {
MYSQL* conn = createConnection();
if (conn) {
connQueue_.push({conn, steady_clock::now()});
++totalConn_;
}
}
// 2. 启动后台回收线程
std::thread t([this] { recycler(); });
t.detach();
}
初始化完成两件事:
- 预创建连接 :循环调用
createConnection(),成功则入队。这样getConnection()首次调用时队列中已有可用连接,不存在冷启动延迟 - 启动回收线程 :
detach分离式后台线程,周期性清理闲置超标的连接,不需要外部管理生命周期
创建连接:MySQL C API
cpp
MYSQL* ConnectionPool::createConnection() {
MYSQL* conn = mysql_init(nullptr); // 1. 分配 MYSQL 对象
if (!conn) return nullptr;
if (!mysql_real_connect(conn, ip_, user_, // 2. TCP 连接 MySQL
password_, dbname_, port_,nullptr, 0)) {
mysql_close(conn); // 失败则释放
return nullptr;
}
return conn; }
两步走:mysql_init 分配 MYSQL 对象,mysql_real_connect 建立真实 TCP 连接。如果连接失败必须 mysql_close 释放已分配的对象,避免内存泄漏。
getConnection:核心获取逻辑
这是连接池最复杂也最关键的部分,整个流程:
getConnection()
│
├─ lock(mutex_)
│
├─ while (队列空 && !stop_)
│ │
│ ├─ totalConn_ < maxConn_?
│ │ ├─ 是 → createConnection() ← 动态扩容
│ │ │ ├─ 成功 → ++totalConn_ → 构造 shared_ptr → return
│ │ │ └─ 失败 → 继续循环
│ │ └─ 否(已达上限)
│ │ └─ cond_.wait_for(connTimeout_) ← 阻塞等归还
│ │ ├─ 超时 → LOG_ERROR → return nullptr
│ │ └─ 被唤醒 → 回到 while 重新检查
│
├─ 从队首取出一个 MYSQL*
│
├─ checkConnection(mysql_ping) ← 健康检查
│ ├─ 存活 → 继续
│ └─ 失效 → mysql_close → 创建替换 → totalConn_ 不变
│
└─ 构造 shared_ptr + 自定义删除器 → return
动态扩容:队列为空但还没到上限
cpp
if (totalConn_ < maxConn_) {
MYSQL* conn = createConnection();
if (conn) {
++totalConn_;
auto deleter = [this](MYSQL* c) { /* 归还逻辑 */ };
return MysqlPtr(conn, deleter);
}
}
当队列为空且未达 maxConn_ 上限时,立即创建新连接而不是等待。这保证了突发高并发时能动态扩容。
为什么在持锁状态下创建连接? createConnection() 可能耗时(网络延迟),但在当前设计中仅当队列为空时才走到创建分支。如果在锁外创建,需要额外逻辑处理竞争(其他线程可能同时在锁外创建,导致超额),当前设计简单正确,建连耗时通常 < 1ms(局域网),阻塞时间可接受。
等待与超时:已达上限只能等
cpp
if (cond_.wait_for(lock, chrono::seconds(connTimeout_))
== cv_status::timeout) {
if (connQueue_.empty()) {
LOG_ERROR("getConnection timeout (%ds)", connTimeout_);
return nullptr;
}
}
当 totalConn_ == maxConn_ 且队列为空时,无法再创建新连接,只能等待其他线程归还:
wait_for阻塞当前线程,等待notify_one唤醒- 超过
connTimeout_秒仍未等到,返回nullptr - 使用 while 循环而非 if,防止虚假唤醒(spurious wakeup)
- 超时后二次检查
connQueue_.empty(),防止通知和超时的时序竞争
健康检查:连接可能已经死了
cpp
if (!checkConnection(conn)) { // mysql_ping() 检测
--totalConn_;
mysql_close(conn); // 关掉坏连接
if (totalConn_ < maxConn_) {
conn = createConnection(); // 自动创建替换
++totalConn_;
} else {
return nullptr;
}
}
数据库连接可能因为 MySQL 重启、网络中断、wait_timeout 超时等原因失效。取出连接后用 mysql_ping() 检测是否还活着:
- 存活 → 正常使用
- 失效 → 关掉坏的,创建新的替换,
totalConn_先减后加保持不变
这保证了用户拿到的连接一定是可用的,不会拿到一个死连接导致 SQL 执行失败。
RAII 自动归还:自定义删除器的精髓
这是连接池设计的点睛之笔------保证连接一定会被归还:
cpp
auto deleter = [this](MYSQL* c) {
if (c) {
std::lock_guard<std::mutex> l(mutex_);
connQueue_.push({c, steady_clock::now()});
cond_.notify_one(); // 唤醒等待的 getConnection()
}
};
return MysqlPtr(conn, deleter);
MysqlPtr 是 shared_ptr<MYSQL>,不是裸指针。自定义删除器在 shared_ptr 析构时自动调用,将连接归还到队列并唤醒等待者。
用户代码极其简单:
cpp
auto conn = pool->getConnection();
if (conn) {
mysql_query(conn.get(), "SELECT * FROM users");
MYSQL_RES* res = mysql_store_result(conn.get());
// ... 处理结果
} // ← shared_ptr 析构 → 连接自动归还池中!
即使用户中途 return 或抛异常,shared_ptr 的 RAII 机制依然保证归还。用户永远不需要手动调用 returnConnection(),从根源上杜绝了连接泄漏。
回收线程:后台清理闲置连接
cpp
void ConnectionPool::recycler() {
while (!stop_) {
this_thread::sleep_for(chrono::seconds(1)); // 每秒扫描一次
lock_guard<mutex> lock(mutex_);
while (totalConn_ > minConn_ && !connQueue_.empty()) {
auto& front = connQueue_.front();
auto idleSec = duration_cast<seconds>(
steady_clock::now() - front.addTime
).count();
if (idleSec >= idleTimeout_) {
mysql_close(front.conn); // 关闭空闲超时连接
connQueue_.pop();
--totalConn_;
} else {
break; // 队首没超时,队列后面的连接空闲时间只会更短,直接退出
}
}
}
}
回收线程每秒扫描一次,从队首开始检查:
addTime记录的是连接归还到队列的时间- 队列是 FIFO,队首是最早归还的(闲置最久的)
- 如果队首连接未超时,后面的肯定没超时,直接
break
回收必须同时满足两个条件:
totalConn_ > minConn_(高于下限,不能把保底连接也收了)idleSec >= idleTimeout_(闲置超时)
这保证了池中始终有至少minConn_个连接随时可用,同时不会无限制堆积空闲连接。
销毁:优雅退出
cpp
void ConnectionPool::destroy() {
stop_ = true; // 1. 设置停止标志
cond_.notify_all(); // 2. 唤醒所有阻塞的 getConnection()
lock_guard<mutex> lock(mutex_);
while (!connQueue_.empty()) { // 3. 关闭所有空闲连接
mysql_close(connQueue_.front().conn);
connQueue_.pop();
--totalConn_;
}
}
单例析构时(程序退出)自动调用。三步走:设标志 → 唤醒等待者 → 关闭所有连接。被唤醒的 getConnection() 会因 stop_ 检查而退出等待循环。
线程安全分析
| 资源 | 保护方法 |
|---|---|
connQueue_ |
std::mutex 全程保护 |
totalConn_ |
只在 mutex_ 锁定下读写 |
| 生产者/消费者协调 | std::condition_variable cond_ |
| 回收线程 | 每 1s 加锁检查,不干扰其他操作 |
stop_ |
在锁保护下读写 |
并发场景推演:
| 场景 | 行为 |
|---|---|
| 10 线程同时获取,池中 5 个空闲 | 5 个拿到空闲,5 个检测到队列空 → 尝试创建(受 totalConn_ < maxConn_ 限制) |
10 线程使用中,达 maxConn_,第 11 个来取 |
队列空 + totalConn_ == maxConn_ → wait_for 阻塞 |
| 一个线程归还连接 | cond_.notify_one() 唤醒等待者 |
| 回收线程扫描 | 加锁检查 idleSec,关闭超出的空闲连接 |
完整生命周期总览
程序启动
│
├─ getInstance() → 获取单例
├─ init(...) → 预创建 minConn 个连接 + 启动 recycler
│
▼
┌────────────────────────────────────────────────┐
│ 运行阶段(循环往复) │
│ │
│ 业务线程 回收线程 │
│ ──────── ──────── │
│ getConnection() 每秒扫描 │
│ ├─ 队列有连接 → 取出 队首 │
│ ├─ 队列空 + 未达上限 → 创建 idle > │
│ └─ 队列空 + 已达上限 → 等待 timeout? │
│ └─ 超时 → return nullptr ↓ │
│ 使用连接执行 SQL 回收多余 │
│ shared_ptr 析构 → 归还到队列 保证 ≥ │
│ └─ notify_one 唤醒等待者 minConn_ │
│ │
└────────────────────────────────────────────────┘
│
▼
程序退出
└─ destroy() → stop_=true → notify_all → 关闭所有连接
设计价值总结
| 设计要点 | 实现方式 | 收益 |
|---|---|---|
| 单例模式 | C++11 Magic Static | 全局唯一池,线程安全初始化 |
| 动态扩容 | 队列空时按需创建 | 突发流量自动适配,不浪费 |
| RAII 归还 | shared_ptr 自定义删除器 | 零泄漏,用户无需手动归还 |
| 健康检查 | mysql_ping + 自动替换 | 用户拿到的连接一定可用 |
| 后台回收 | recycler 线程 + FIFO 队首检查 | 空闲连接自动瘦身,保底下限 |
| 超时等待 | wait_for + while 循环 | 防止无限阻塞,防虚假唤醒 |
系列串联
连接池是 muduo 网络库的扩展模块------它不改变 muduo 的 Reactor 架构,而是作为数据层的配套设施,让高性能网络层不会因为数据库建连开销而浪费 CPU 和 IO。至此,从底层的 Channel、Poller、EventLoop,到中层的 Acceptor、Buffer、TcpServer、TcpConnection,再到扩展层的连接池,一个完整的 C++ 高并发服务器基础设施已经齐备。下一篇将把所有模块整合起来,做一个完整的模块梳理总结。