问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

MySQL数据库连接池实现详解(C++版)

创作时间:
作者:
@小白创作中心

MySQL数据库连接池实现详解(C++版)

引用
CSDN
1.
https://blog.csdn.net/weixin_45957158/article/details/142106960

本文将介绍MySQL数据库连接池的实现方法,包括同步和异步连接池的原理和实现细节。通过本文,读者可以深入了解数据库连接池的工作机制,并掌握其在实际项目中的应用方法。

0. 前言

上一篇文章介绍了线程池的实现,这次将介绍Linux下数据库连接池的实现,以MySQL为例。由于连接池的实现较为复杂,本次将以一个开源框架的数据库连接池部分为例进行讲解。

1. 数据库连接池概述

1.1 服务器与数据库交互

服务器与数据库的交互采用请求响应模式,通常使用TCP长连接。TCP连接需要三次握手和四次挥手,每次连接都需要验证账号密码等,因此连接资源属于耗时资源,适合使用连接池来复用连接。

1.2 MySQL数据库网络模型

MySQL数据库的网络模型如下:

  • 主线程使用IO多路复用中的select来监听listenfd
  • 当listenfd上触发可读事件时,说明有客户端来连接,就为他分配一个连接线程
  • 同一个连接上,如果收到多个请求,该连接线程是串行执行的

这里可能会有疑问:为什么MySQL用的是select,而不是性能更高的epoll?主要原因是select支持跨平台,而epoll只是Linux下的。另外,MySQL规定最高只能监听128个文件描述符,在这种小数量下,select和epoll的性能是差不多的。

所以当我们创建多条连接时,每条连接MySQL都会分配一个线程,可以充分释放MySQL执行SQL语句的性能。但是,创建的连接数量并不是越多越好,一般数量控制在CPU核心数比较合适,创建的太多反而会降低性能。

1.3 MySQL连接驱动安装

如果要用C/C++实现数据库连接池,首先要安装libmysqlclient-dev驱动。这个官方提供的驱动使用了阻塞IO,具体来说:

  • 首先将sql通过MySQL协议打包
  • 然后服务器调用send()或write()
  • 然后调用recv()会阻塞线程等待MySQL的返回
  • 最后确定是完整回应包后进行协议解析并将结果返回

协议打包和解析都是MySQL驱动帮我们完成的,所以可以很明显看到这是个耗时操作,一般主线程在处理业务逻辑需要和数据库交互的时候,这种阻塞的耗时操作都需要优化。

1.4 同步(synchronous)连接池与异步(asynchronous)连接池

同步和异步的概念不好理解,经常要和阻塞和非阻塞搞混。这里以后面要实现的两个接口为例:

QueryResult Query(char const* sql, T* connection = nullptr);
QueryCallback AsyncQuery(char const* sql);

第一个Query()是同步连接池的接口,另一个AsyncQuery()是异步连接池的接口。同步和异步的区别就在于执行一条SQL语句后,怎么拿到数据库的返回值:

  • 在同步连接池中,Query()一返回,就可以拿到数据库的返回值
  • 在异步连接池中,执行SQL并不是发生在AsyncQuery()中,AsyncQuery()返回的结果也不是数据库的返回值,当前的职责是在其他接口中实现的

这里要区分阻塞与同步,非阻塞与异步的区别:

  • 阻塞是指的线程或者协程,在等待某个事件时无法继续工作
  • 同步是指的任务按顺序执行,一个完成后才能执行下一个
  • 非阻塞和异步也是同样的区别,阻塞是实现同步的一种方式,但不是唯一的

给大家看一下两个接口的具体实现函数:

QueryResult DatabaseWorkerPool<T>::Query(char const* sql, T* connection /*= nullptr*/)
{
    if (!connection)
        connection = GetFreeConnection();
    ResultSet* result = connection->Query(sql);
    connection->Unlock();
    if (!result || !result->GetRowCount() || !result->NextRow())
    {
        delete result;
        return QueryResult(nullptr);
    }
    return QueryResult(result);
}

其中Query(char const* sql, T* connection /= nullptr/)调用了Query(char const* sql),Query(char const* sql)又调用了_Query(const char* sql, MySQLResult pResult, MySQLField pFields, uint64* pRowCount, uint32* pFieldCount),_Query(const char* sql, MySQLResult pResult, MySQLField pFields, uint64* pRowCount, uint32* pFieldCount)中调用了mysql_query()这个数据库提供的阻塞函数。所以该函数最终返回的结果就是数据库的返回值。

再来看看异步连接的实现:

QueryCallback DatabaseWorkerPool<T>::AsyncQuery(char const* sql)
{
    BasicStatementTask* task = new BasicStatementTask(sql, true);
    // Store future result before enqueueing - task might get already processed and deleted before returning from this method
    QueryResultFuture result = task->GetFuture();
    Enqueue(task);
    return QueryCallback(std::move(result));
}

可以看到是把任务放到了一个队列中,实际上是交给了线程池来做,不阻塞当前线程,去阻塞线程池中的一个线程来获取数据库的返回值。

1.5 同步连接池和异步连接池的使用场景

异步连接池的性能要高于同步连接池,那什么时候适合使用同步连接池呢?

  • 在服务器初始化阶段,因为这一阶段许多操作是线性、依赖关系明确的,如初始化数据库连接、加载配置、启动服务等,这些操作通常需要按照严格的顺序执行,确保前面的步骤完成后,后续步骤才能安全进行。如果使用异步机制,虽然可以提升并发性能,但可能会引入复杂的错误处理逻辑,增加调试的难度
  • 所以服务器启动过程的可靠性优先于性能,并且短暂的初始化过程中也没有大量并发需求,而同步正好可以让任务顺序执行,适合这一阶段
  • 除此之外,在处理业务时都适合用异步连接池

1.6 实现同步连接池与异步连接池

1.6.1 实现同步连接池

示意图如上所示,开启多个线程加快服务器初始化,加锁的目的是为了不让多个线程同时使用一个连接,同时是否上锁也是该连接是否空闲的标志。获取连接的方式是round robin(轮询算法),依次查看连接是否空闲,发现空闲连接就上锁,与数据库进行交互,阻塞该线程等待数据库返回,返回之后释放锁。

1.6.2 实现异步连接池

示意图如上所示,基于线程池(对线程池不熟悉的同学,可以去看我另一篇实现线程池的文章)实现,用户请求push进SQL任务执行队列,让线程池中的线程去取任务并与数据库交互,从而实现不阻塞主线程,去阻塞线程池中的线程,与传统的线程池不同的是,一般线程池中线程会因为任务队列为空而阻塞,异步连接池中的线程阻塞除了上述原因外,还可能是为了等待数据库返回而阻塞。线程池中的连接是线程安全的,每个连接都和特定的线程一一绑定,而具体的任务与连接是无关的。

2. 实现数据库连接池

代码来自于一个名为TrinityCore的项目,该项目是一个开源的MMORPG服务端模拟器,重点来分析该项目中数据库连接池的设计,主要剖析的主文件是DatabaseWorkerPool.h、DatabaseWorkerPool.cpp。

首先要明确一个概念,MySQL中很多数据库,一个连接池对应着一个库,如果需要访问两个库,就需要两个连接池,以此类推。

2.1 初始化数据库连接池连接信息

相关函数和类定义如下:

// 设置数据库连接信息,包括数据库连接字符串和异步、同步线程的数量
void SetConnectionInfo(std::string const& infoString, uint8 const asyncThreads, uint8 const synchThreads);
void DatabaseWorkerPool<T>::SetConnectionInfo(std::string const& infoString, uint8 const asyncThreads, uint8 const synchThreads)
{
    _connectionInfo = std::make_unique<MySQLConnectionInfo>(infoString);
    _async_threads = asyncThreads;
    _synch_threads = synchThreads;
}
struct TC_DATABASE_API MySQLConnectionInfo
{
    explicit MySQLConnectionInfo(std::string const& infoString);
    std::string user;
    std::string password;
    std::string database;
    std::string host;
    std::string port_or_socket;
    std::string ssl;
};

可以看到首先初始化数据库连接信息(包括用户名、密码、哪个库等),然后设置同步和异步的线程数量。

2.2 SQL字符串与预处理的SQL语句

void Execute(char const* sql); // 简单字符串,如“select * from table;”
void Execute(PreparedStatement<T>* stmt); // 预处理好的SQL语句

这里简单的说一下预处理,预处理是指在执行SQL查询之前,将SQL语句的结构进行预编译和缓存,以便多次执行,提高效率并增强安全性。具体来说,预处理可以提高执行效率,对于重复执行的SQL语句,预处理能够提高执行效率。SQL语句只需要编译一次,以后每次执行时只需要传递参数并执行,节省了重新编译的开销。且预处理可以防止SQL注入,预处理可以有效防止SQL注入攻击。因为用户提供的输入是作为参数绑定到预编译的SQL语句中的,攻击者无法通过输入恶意的SQL代码来篡改查询逻辑。例如,如果使用预处理,攻击者不能通过输入

1 OR 1=1

来破坏查询逻辑,因为这会被视为一个普通的字符串参数,而不是SQL代码。

2.3 函数接口封装和调用关系

上图为总体的核心的函数调用关系,大家只要对线程池有充分的理解,就可以很容易看懂这里面的调用关系。源代码比较多,但是核心的部分就在这里面,代码虽多但很好理解,完整代码我放在了数据库连接池源代码,感兴趣的同学可以去自己阅读以下

3. 结束语

本文较为简单的阐述了数据库连接池的实现方法,本人目前还是个在校生,还比较小白,也刚刚开始写CSDN博客不久,可能写的也不是很好,如果有任何疑问或者发现我有哪里写的不对的地方,欢迎大家留言告诉我!我都会一一改正的。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号