前置知识: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 → 关闭连接」的流程,会有严重的性能问题:

  1. 性能开销大:每次连接都要 TCP 三次握手、MySQL 认证、权限校验,耗时几毫秒到几十毫秒,高并发下会成为性能瓶颈;

  2. 连接数失控:如果同时有几百上千个线程访问数据库,会创建几百上千条连接,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(),会有两个严重问题:

  1. 容易忘记归还:业务代码复杂,分支多,很容易漏掉 FreeConn,导致连接永远被占用,池子里的连接越来越少,最终耗尽(连接泄漏);

  2. 异常不安全:如果获取连接后、归还前,代码抛出了异常,后面的 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

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐