muduo网络库(十六):新增连接池模块

muduo网络库(十六):新增连接池模块

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();
                                                                                            }

初始化完成两件事:

  1. 预创建连接 :循环调用 createConnection(),成功则入队。这样 getConnection() 首次调用时队列中已有可用连接,不存在冷启动延迟
  2. 启动回收线程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);

MysqlPtrshared_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
    回收必须同时满足两个条件:
  1. totalConn_ > minConn_(高于下限,不能把保底连接也收了)
  2. 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++ 高并发服务器基础设施已经齐备。下一篇将把所有模块整合起来,做一个完整的模块梳理总结。

相关推荐
「QT(C++)开发工程师」24 分钟前
C++11 std::unique_ptr — 独占所有权的智能指针
c语言·开发语言·c++·qt
不正经学生1 小时前
C语言动态内存管理(上):堆上的自由与责任
c语言·开发语言·c++·算法·面试
aiot189189352181 小时前
机场候机大厅高空场景技术红线!蓝牙AOA不能做手机导航??!!
大数据·网络·人工智能·蓝牙aoa
从入门到退休1 小时前
企业远程控制选型:向日葵SDK vs RustDesk自建,谁是更务实的选择?
运维·服务器·网络·远程工作·远程控制
zl.rs1 小时前
配置更适合生产环境的coredump
c++
fb_123452 小时前
网络技术基础
网络
一木 之林2 小时前
三.C++ 内存管理(进阶)(二)
开发语言·arm开发·c++
DLite2 小时前
开源一个静态类型语言——NLang
c++·编译器·nlang
uoKent2 小时前
c++中new和malloc的区别
java·jvm·c++