LiteHub之数据库连接池
Qingyh Lv2

理论部分

不使用数据库连接池

我们先来看看普通的Mysql的连接过程,下图是我抓包分析的在端口3306的数据包:
image
在执行Mysql命令之前,需要先经过Tcp的三次握手、Mysql的认证服务,TLS加密服务等操作;
下图来自数据库连接池学习笔记(一):原理介绍+常用连接池介绍
image
经过上述分析,我们知道,如果不使用数据库连接池,执行单条Mysql命令会多了非常多我们不关心的网络交互。
如果执行的Mysql查询命令比较多,就会严重影响性能。

不使用数据库连接池:
优点: 实现简单
缺点: 网络IO较多
数据库的负载较高
响应时间较长及QPS较低
应用频繁的创建连接和关闭连接,导致临时对象较多
在关闭连接后,会出现大量TIME_WAIT 的TCP状态(在2个MSL之后关闭)

使用数据库连接池

image

初始连接建立
当应用程序第一次访问数据库时,需要完成以下步骤:

  1. 系统会创建一个新的数据库连接
  2. 这个连接需要经过身份验证(用户名/密码验证)
  3. 建立TCP/IP网络连接
  4. 初始化会话参数和设置

连接复用机制
在后续访问中:

  1. 系统会从连接池中获取先前建立的可用连接
  2. 直接使用该连接执行SQL语句(如SELECT, INSERT, UPDATE等)
  3. 执行过程中无需重新进行身份验证和连接建立
  4. 典型的复用场景包括:
    • 用户多次查询同一数据表
    • 处理事务中的多个SQL操作
    • 执行批量数据处理任务

连接回收过程

每次查询完成后:

  1. 系统会将连接标记为”空闲”状态
  2. 连接会被归还到连接池中
  3. 连接保持开启状态,等待下次请求
  4. 如果连接空闲时间超过配置的阈值(如30分钟),可能会被自动关闭

这种机制显著提高了性能,减少了频繁创建和销毁连接的开销。

常见的几种资源池

在实际应用中,由于创建和销毁系统资源(如连接、内存块、线程等)的成本往往远高于使用资源的成本,因此通常会引入资源池的概念来提高系统性能。这种技术通过预先创建并维护一组可重用资源,避免了频繁的资源初始化和销毁操作,从而显著提升系统效率。

常见的资源池包括以下几种类型:

  1. 内存池

    • 在C++程序开发中,malloc通过brk()系统调用向操作系统申请内存时,会一次性申请较大的内存块(只是通过brk方式申请的内存会维护内存池,小块内存;通过nmap方式申请的大块内存(默认是大于128k)没有内存池,是直接归还给操作系统了)
    • 当使用free释放内存时,这些内存并不会立即归还给操作系统,而是被缓存在malloc维护的内存池中
    • 当下次程序再次申请内存时,malloc会优先从内存池中分配可用内存块
    • 例如:当程序频繁进行小内存块的分配和释放时,内存池可以避免频繁的系统调用,提高内存分配效率
  2. 线程池

    • 传统的线程创建和销毁涉及操作系统层面的资源分配和回收,开销较大
    • 线程池通过预先创建一组线程并保持活跃状态,等待任务分配
    • 主要优势包括:
      • 线程复用:避免频繁创建销毁线程的开销
      • 任务解耦:将任务提交与执行分离,提高系统灵活性
      • 资源管理:可以限制并发线程数量,防止系统过载
    • 应用场景:Web服务器处理请求、批量数据处理等需要高并发的场景
  3. 数据库连接池

    • 建立数据库连接涉及网络通信、身份验证等耗时操作
    • 连接池维护一组已建立的数据库连接,应用程序使用时直接从池中获取
    • 使用完毕后连接归还池中而非关闭,供其他请求复用
    • 典型配置参数包括:最小连接数、最大连接数、连接超时时间等
    • 优势:显著降低连接建立开销,提高数据库访问效率

这些资源池技术在现代软件系统中被广泛应用,特别是在高并发、高性能要求的场景下,合理配置资源池可以大幅提升系统整体性能。在实际应用中,由于创建和销毁系统资源(如连接、内存块、线程等)的成本往往远高于使用资源的成本,因此通常会引入资源池的概念来提高系统性能。这种技术通过预先创建并维护一组可重用资源,避免了频繁的资源初始化和销毁操作,从而显著提升系统效率。

代码实现

本项目数据库连接池实现了三个核心类:

  • DbConnection:管理单个数据库连接
  • DbConnectionPool:管理数据库连接池
  • MysqlUtil:提供便捷的数据库操作接口

以下就分别来了解一下这几个类的实现。

DbConnection类实现

成员变量

  • std::shared_ptr<sql::Connection> conn_ 数据库连接
  • std::string host_ 数据库主机地址,如tcp://127.0.0.1:3306
  • std::string user_ 用户名
  • std::string password_ 密码
  • std::string database_ 使用数据库
  • std::mutex mutex_ 互斥锁

成员方法

  • DbConnection()构造函数, 创建并初始化数据库连接,设置连接属性(这里是设置的单语句执行,防止SQL的注入)
  • ~DbConnection()析构函数,自动清理连接资源,调用cleanup()函数
  • ping()函数,使用简单的SELECT 1语句检测与数据库的通信是否正常
  • isValid()函数,与ping函数类似,区别在于不在意查询结果,遇到异常返回false
  • reconnect() 函数,尝试重新建立数据库连接
  • cleanup()函数,清理连接状态,需要确保所有事务以及完成,并且消费完所有查询结果
  • bindParams(),绑定参数
  • executeQuery()函数,执行sql语句的查询,并返回查询结果
  • executeUpdate()函数,执行sql语句的更新操作

在上述函数中比较重要的就是executeQuery()executeUpdate()函数,以下是其代码定义与注释

executeQuery()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template<typename... Args> //可变参数模板,接受任意数量、任意类型的参数
sql::ResultSet* executeQuery(const std::string& sql, Args&&... args)
{
std::lock_guard<std::mutex> lock(mutex_); // 确保线程安全
try
{
// 直接创建新的预处理语句,不使用缓存
std::unique_ptr<sql::PreparedStatement> stmt(
conn_->prepareStatement(sql)
);
bindParams(stmt.get(), 1, std::forward<Args>(args)...); // 绑定参数,std::forward 确保完美转发
return stmt->executeQuery(); //执行后返回 sql::ResultSet*
}
catch (const sql::SQLException& e)
{
LOG_ERROR << "Query failed: " << e.what() << ", SQL: " << sql;
throw DbException(e.what());
}
}

这个是执行Sql查询的操作,在上层,通过代码

1
2
std::string sql = "SELECT id FROM users WHERE username = ? AND password = ?";
sql::ResultSet* res = mysqlUtil_.executeQuery(sql, username, password);

传入了sql语句:”SELECT id FROM users WHERE username = ? AND password = ?”
以及两个参数:username=”user1“和password=”123456“;
通过参数绑定后,完整的sql语句就是
“SELECT id FROM users WHERE username = user1 AND password =123456”
随后执行Mysql的语句查询,返回查询到的结果。

executeUpdate函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template<typename... Args>
int executeUpdate(const std::string& sql, Args&&... args)
{
std::lock_guard<std::mutex> lock(mutex_);
try
{
// 直接创建新的预处理语句,不使用缓存
std::unique_ptr<sql::PreparedStatement> stmt(
conn_->prepareStatement(sql)
);
bindParams(stmt.get(), 1, std::forward<Args>(args)...);
return stmt->executeUpdate();
}
catch (const sql::SQLException& e)
{
LOG_ERROR << "Update failed: " << e.what() << ", SQL: " << sql;
throw DbException(e.what());
}
}

示例语句:

1
2
3
const std::string sql = "INSERT INTO video_stats (video_name, view_count, like_count) VALUES (?, 0, 0) ON DUPLICATE KEY UPDATE video_name=video_name";
int affected = mysqlUtil_.executeUpdate(sql, video_name);

SQL注入的理解
当用户登录网站时,通常会输入用户名和密码。
以下是一段正常的 SQL 查询代码:

1
SELECT * FROM users WHERE username = 'user1' AND password = 'password1';

如果攻击者输入:

1
2
用户名: admin' --
密码: anything

SQL查询变成:

1
SELECT * FROM users WHERE username = 'admin' --' AND password = 'anything';

其中 – 是 SQL 的注释符号,忽略了密码条件,直接绕过了身份验证。
更多可以参考
SQL 注入

DbConnectionPool类实现

成员变量:

  • std::string host_; 连接数据库的主机名
  • std::string user_; 用户名
  • std::string password_; 密码
  • std::string database_; 指定使用的数据库
  • std::queue<std::shared_ptr<DbConnection>> connections_; 数据库连接池
  • std::mutex mutex_; 互斥锁
  • std::condition_variable cv_; 条件变量
  • bool initialized_ = false; 初始化标识,确保仅初始化一次
  • std::thread checkThread_; // 添加检查线程,检测数据库连接池的连接健康状况

成员方法:
使用单例模式,

  • init()函数,初始化线程池,创建 poolSize 个 DbConnection 对象,放入队列
  • DbConnectionPool()构造函数,创建并分离后台线程,定期检查连接可用性
  • ~DbConnectionPool()析构函数,清空连接队列,释放所有连接资源
  • getConnection() 函数,从连接池获取一个可用连接
  • createConnection() 函数,创建一个新的数据库连接
  • checkConnections() ,后台线程,定期检查所有连接是否可用。

这里比较重要的就是getConnection() 函数,我贴出来代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
std::shared_ptr<DbConnection> DbConnectionPool::getConnection() 
{
std::shared_ptr<DbConnection> conn;
{
std::unique_lock<std::mutex> lock(mutex_);
// 如果连接池为空,则阻塞等待其他线程释放连接
while (connections_.empty())
{
if (!initialized_)
{
throw DbException("Connection pool not initialized");
}
LOG_INFO << "Waiting for available connection...";
cv_.wait(lock); // 等待条件变量通知
}
// 从队列中取一个连接
conn = connections_.front();
connections_.pop();
} // 释放锁

try
{
// 在锁外检查连接
if (!conn->ping())
{
LOG_WARN << "Connection lost, attempting to reconnect...";
conn->reconnect();
}

// 使用自定义 deleter:
// 当外部 shared_ptr 被释放时,把连接放回连接池并通知等待线程
return std::shared_ptr<DbConnection>(conn.get(),
[this, conn](DbConnection*) {
std::lock_guard<std::mutex> lock(mutex_);
connections_.push(conn);
cv_.notify_one();
});
}
catch (const std::exception& e)
{
// 如果重连失败,则把连接放回队列并通知等待线程
LOG_ERROR << "Failed to get connection: " << e.what();
{
std::lock_guard<std::mutex> lock(mutex_);
connections_.push(conn);
cv_.notify_one();
}
throw;
}
}

函数的核心逻辑为:

  • 如果连接池为空,则阻塞等待(使用条件变量)
  • 连接池始终返回 std::shared_ptr<DbConnection>
  • 通过自定义的deleter在用户使用完数据库某条连接后自动归还给池
    使用lambda表达式,当conn使用完成后,将其归还到connections_池中去。
1
2
3
4
5
6
7
8
// 使用自定义 deleter:
// 当外部 shared_ptr 被释放时,把连接放回连接池并通知等待线程
return std::shared_ptr<DbConnection>(conn.get(),
[this, conn](DbConnection*) {
std::lock_guard<std::mutex> lock(mutex_);
connections_.push(conn);
cv_.notify_one();
});

MysqlUtil类实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void init(const std::string& host, const std::string& user,
const std::string& password, const std::string& database,
size_t poolSize = 10)
{ //调用了 DbConnectionPool 的单例模式,保证全局唯一
http::db::DbConnectionPool::getInstance().init(
host, user, password, database, poolSize);
}

template<typename... Args>
sql::ResultSet* executeQuery(const std::string& sql, Args&&... args)
{ //对外提供 执行查询(SELECT) 的接口
//使用者不需要显式创建 DbConnection,也不用关心 getConnection 和归还连接的逻辑
auto conn = http::db::DbConnectionPool::getInstance().getConnection();
return conn->executeQuery(sql, std::forward<Args>(args)...);
}

template<typename... Args>
int executeUpdate(const std::string& sql, Args&&... args)
{
//对外提供 执行更新(INSERT / UPDATE / DELETE) 的接口
auto conn = http::db::DbConnectionPool::getInstance().getConnection();
return conn->executeUpdate(sql, std::forward<Args>(args)...);
}

MysqlUtil类的实现比较简单,提供了对数据库的简单接口,隐藏了底层连接池的复杂性。主要是提供了三个方法,分别是数据库连接池的初始化、从连接池中获取连接以执行查询操作和更新(增删改)操作。MysqlUtil作为一个便捷的工具类,简化了调用接口,让业务层可以更轻松的使用连接池进行增删改查的工作。

以上就是我对数据库连接池的一些理解,如有不当之处,敬请指出。

由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 31k