MySQL同步连接池与TrinityCore的对比学习(六)
一、 同步连接池实现
在TrinityCore
中,数据库连接池的实现采用了两种类型组合方式:异步和同步。这两种类型分别用于处理不同类型的SQL操作,以满足不同的性能需求;先聚焦于同步连接池的实现机制。
1.1、总体设计思路
采用"一请求对应一连接,一连接对应一把锁"的设计理念,业务处理完毕后立即释放连接资源。
1.2、存储结构设计
// 在 DatabaseWorkerPool.h 中
enum InternalIndex
{IDX_ASYNC, // 异步连接IDX_SYNCH, // 同步连接 IDX_SIZE
};
......
std::array<std::vector<std::unique_ptr<T>>, IDX_SIZE> _connections;
......
_connections
为大小为 2 的数组,分别对应异步和同步连接类型- 每个数组元素使用
std::vector
存储数据库连接 - 采用
std::unique_ptr
智能指针管理连接生命周期
1.3、获取连接
template <class T>
T* DatabaseWorkerPool<T>::GetFreeConnection()
{uint8 i = 0;auto const num_cons = _connections[IDX_SYNCH].size();T* connection = nullptr;// 轮询查找可用连接for (;;){connection = _connections[IDX_SYNCH][++i % num_cons].get();// 尝试锁定连接,如果成功就使用if (connection->LockIfReady())break;}return connection;
}bool MySQLConnection::LockIfReady()
{return m_Mutex.try_lock(); // 非阻塞尝试获取锁
}
- 轮询遍历所有连接
- 通过
LockIfReady
尝试非阻塞锁定连接,如果成功则返回该连接;否则立即尝试下一个连接
1.4、资源管理模式
严格遵循 锁定-使用-释放 模式:
QueryResult DatabaseWorkerPool<T>::Query(char const* sql, T* connection)
{if (!connection)connection = GetFreeConnection(); // 获取连接// 执行数据库操作ResultSet* result = connection->Query(sql);connection->Unlock(); // 释放连接// ... 处理结果 ...
}
二、与之前实现的同步连接池对比
在之前也实现过一版同步连接池,和TrinityCore
的实现相比较,有以下不同点:
之前版本的同步连接池
2.1、智能指针选择策略
实现方案 | 个人实现 | TrinityCore |
---|---|---|
智能指针 | std::queue<std::shared_ptr<sql::Connection>> m_freeConnPool | std::array<std::vector<std::unique_ptr<T>>, IDX_SIZE> _connections |
- 为什么选择
unique_ptr
而非shared_ptr
?
我个人猜想,unique_ptr
相较于shared_ptr
有以下优势:
-
性能优势
- unique_ptr 无原子操作开销,性能接近原始指针
- shared_ptr 的引用计数机制在高度并发场景下存在性能瓶颈
-
所有权明确
- unique_ptr 强制单一所有权,避免资源管理 ambiguity
- 编译时检查确保资源安全
-
内存安全
- 避免循环引用导致的内存泄漏
- 简化资源生命周期管理
2.2、连接获取策略
TrinityCore
采用的是轮询策略,即依次尝试每个连接。
// 轮询 + 非阻塞锁定
for (;;) {connection = _connections[IDX_SYNCH][++i % num_cons].get();if (connection->LockIfReady()) // try_lockbreak;
}
- 个人采用的是
条件变量等待
// 条件变量等待
if(m_freeConnPool.empty()){m_condition.wait(lock, [this](){ return !m_freeConnPool.empty(); });
}
auto conn = m_freeConnPool.front();
m_freeConnPool.pop();
性能分析:
- TrinityCore:无上下文切换,CPU占用稍高但响应更快
- 个人实现:线程休眠节省CPU,但有唤醒开销
2.3、连接归还策略
TrinityCore
采用的是手动解锁
connection->Unlock();
- 个人采用的是
shared_ptr
的自定义删除器归还
auto conn = std::shared_ptr<sql::Connection>(m_driver->connect(...),[this](sql::Connection* conn){returnConnection(std::shared_ptr<sql::Connection>(conn));}
);
TrinityCore
的实现,需要程序员非常小心地管理连接的锁定和解锁,稍有不慎就可能导致死锁或资源泄露。
而个人实现通过RAII自动管理,避免资源泄漏
2.4、连接健康管理
TrinityCore
目前所看的实现中没有看到对连接健康状态的检测,例如是否断开、是否需要重连等。- 个人采用的是自定义删除器中检测
// 获取时检查连接有效性
if(!conn->isValid() || conn->isClosed()){conn->reconnect(); // 尝试重连// 或者创建新连接
}
后续可以考虑在TrinityCore
的实现中加入对连接健康状态的检测,例如在获取连接时检查是否断开并尝试重连。这样可以提高连接的稳定性和可靠性。
2.5、锁粒度设计
-
TrinityCore:
- 细粒度锁:每个连接独立互斥锁
- 高并发场景下竞争较少
-
个人实现:
- 粗粒度锁:整个连接池共用一把锁
- 实现简单但并发性能受限
在性能敏感的场景下,细粒度锁通常比粗粒度锁更优。因为细粒度锁可以减少锁竞争和等待时间,从而提高并发性能。
Tips: 看了下TrinityCore
的部分实现,原本以为只单独将DatabaseWorkerPool
的实现拉下来就可以使用,但没那么简单,除非重新封装DataBaseWorkerPool
,但这样工作量不小。
三、总结:
- 什么时候考虑使用
unique_ptr
,什么时候考虑使用shared_ptr
?
- 使用
unique_ptr
:- 性能敏感的场景,如频繁创建和销毁对象的场合。— 无原子操作开销
- 明确单一所有权的场合,避免悬挂指针和内存泄漏。
- 避免循环引用,简化内存管理。
- 使用
shared_ptr
:- 确实需要共享所有权的场合,例如多个对象需要共同访问同一资源。
- 资源生命周期复杂,难以用单一所有者模型管理的场合。
- 对性能要求不极端的场合,特别是在引用计数操作的开销可以接受的情况下。
- 两种同步连接池对比
特性 | TrinityCore | 个人自定义实现 |
---|---|---|
设计目标 | 极致性能,游戏服务器场景 | 通用性,易用性 |
资源管理 | 手动控制,开发者负责 | 自动管理,RAII 原则 |
错误处理 | 操作失败后恢复 | 健康检查 |
适用场景 | 高性能应用 | 通用应用 |