利用多线程和智能指针手写数据库连接池
项目背景
为了提高MySQL数据库(基于客户端服务端模型)的访问瓶颈
策略一:减小磁盘的IO。
在服务器端增加缓存服务器缓存常用的数据(例如redis)
可以增加连接池,来提高MySQL Server的访问效率。
在高并发情况下,大量的TCP三次握手、MySQL Server连接认证、MySQL Server关闭连接回收资源和TCP四次挥手所耗费的性能时间也是很明显的,增加连接池就是为了减少这一部分的性能损耗。
本项目就是为了在C/C++项目中,提高MySQL Server的访问效率,实现基于C++代码的数据库连接池模块。
什么是连接池
数据库连接池的解决方案是在应用程序启动时建立足够的数据库连接,并讲这些连接组成一个连接池,由应用程序动态地对池中的连接进行申请、使用和释放。对于多于连接池中连接数的并发请求,应该在请求队列中排队等待。并且应用程序可以根据池中连接的使用率,动态增加或减少池中的连接数。
连接池一般包含了数据库连接所用的ip地址、port端口号、用户名和密码以及其它的性能参数,例如初始连接量,最大连接量,最大空闲时间、连接超时时间等。
初始连接量(initSize):表示连接池事先会和MySQL Server创建initSize个数的connection连接,当应用发起MySQL访问时,不用再创建和MySQL Server新的连接,直接从连接池中获取一个可用的连接就可以,使用完成后,并不去释放connection,而是把当前connection再归还到连接池当中。
最大连接量(maxSize):当并发访问MySQL Server的请求增多时,初始连接量已经不够使用了,此时会根据新的请求数量去创建更多的连接给应用去使用,但是新创建的连接数量上限是maxSize,不能无限制的创建连接,因为每一个连接都会占用一个socket资源,一般连接池和服务器程序是部署在一台主机上的,如果连接池占用过多的socket资源,那么服务器就不能接收太多的客户端请求了。当这些连接使用完成后,再次归还到连接池当中来维护。
最大空闲时间(maxIdleTime):当访问MySQL的并发请求多了以后,连接池里面的连接数量会动态增加,上限是maxSize个,当这些连接用完再次归还到连接池当中。如果在指定的maxIdleTime里面,这些新增加的连接都没有被再次使用过,那么新增加的这些连接资源就要被回收掉,只需要保持初始连接量initSize个连接就可以了。
连接超时时间(connectionTimeout):当MySQL的并发请求量过大,连接池中的连接数量已经到达maxSize了,而此时没有空闲的连接可供使用,那么此时应用从连接池获取连接无法成功,它通过阻塞的方式获取连接的时间如果超过connectionTimeout时间,那么获取连接失败,无法访问数据库。该项目主要实现上述的连接池四大功能。
MYSQL部分的连接实现
MySQL数据库C++代码封装
1 |
|
在官方提供的基础上进行功能增加:
1 | class Connection |
连接池实现

连接池只需要一个实例,所以ConnectionPool以单例模式进行设计
从ConnectionPool中可以获取和MySQL的连接Connection
空闲连接Connection
全部维护在一个线程安全的Connection队列
中,使用线程互斥锁
保证队列的线程安全如果Connection队列为空,还需要再获取连接,此时需要动态创建连接,上限数量是maxSize.因此这里是一个生产者线程,生产连接。
队列中空闲连接时间超过maxIdleTime的就要被释放掉,只保留初始的initSize个连接就可以了。也就是说需要一个定时线程清理队列多余的空闲连接
如果Connection队列为空,而此时连接的数量已达上限maxSize,那么等待connectionTimeout时间如果还获取不到空闲的连接,那么获取连接失败,此处从Connection队列获取空闲连接,可以使用带超时时间的mutex互斥锁来实现连接超时时间
用户获取的连接用shared_ptr智能指针来管理,用lambda表达式定制连接释放的功能(不真正释放连接,而是把连接归还到连接池中)
连接的生产和连接的消费采用生产者-消费者线程模型来设计,使用了线程间的同步通信机制条件变量和互斥锁
线程安全的单例
这里采用的是线程安全的懒汉式单例模式。
什么是懒汉模式:在程序运行到需要用到该类的实例化时,instance()方法才去判断单例指针p,进而实例化单例指针p,让人它有一种懒惰,不到最后关头不实例化的感觉。
首先,将类的构造函数进行私有化
1 | private: |
为了实现线程安全的懒汉式单例模式,可以采用锁,也可以利用静态区。这里采用的是后者。使用在静态数据区实现类的实现的方法,因为分配在静态区中所以无论获取实例的函数被调用多少次,所得的都是同一份实例。
1 | public: |
根据配置文件配置参数
1 | // 从配置文件中加载配置项 |
构造函数
构造函数只执行一次
1 |
|
部分解释:
1 | std::thread produce(std::bind(&ConnectionPool::produceConnectionTask, this)); |
这行代码创建了一个名为 produce
的线程对象。它使用
std::bind
函数将
ConnectionPool::produceConnectionTask
成员函数绑定到当前对象实例 (this
) 上,然后传递给
std::thread
构造函数。这样,线程 produce
将执行 ConnectionPool
类produceConnectionTask
成员函数。
调用 detach
函数,将线程 produce
从主线程中分离。这意味着一旦主线程结束,不再等待 produce
线程的完成。线程的生命周期将独立于主线程,允许它在后台继续执行。通常,detach
被用于在主线程退出时,确保所有线程都能完成它们的任务。
生产者线程:连接生产者
首先这个线程一直在查看并创建连接,因此应该在一个死循环里面
1 | void ConnectionPool::produceConnectionTask() |
设置条件变量作为成员变量,用于连接生产线程和连接消费线程的通信
1 | condition_variable cv; |
核心逻辑:如果队列不空,则生产者线程等待暂不生产,否则,当连接数量没有达到上限的时候生产新的连接,生产完后通知消费者
使用互斥锁维护队列之间的线程安全:
1 | mutex _queueMutex; // 维护连接队列的线程安全互斥锁 |
std::mutex
是 C++11
标准引入的互斥量类,用于实现线程同步。互斥量用于保护共享资源,确保同一时间只有一个线程可以访问它。
这行代码的含义是创建一个 std::unique_lock
对象
lock
,该对象对 _queueMutex
进行独占性的锁定。在 lock
对象的生命周期内,_queueMutex
将一直被锁定,直到
lock
被销毁(通常是离开作用域时)或显式调用
unlock
方法来释放锁。这样做的目的是确保在互斥量保护的临界区内,只有一个线程能够执行,防止多个线程同时访问共享资源导致竞态条件。
这里生产者加锁后,消费者就拿不到这把锁了
线程安全队列
std::queue
是 C++
标准模板库(STL)中的队列容器,但它本身并不提供线程安全性。在多线程环境中,多个线程可能会同时访问和修改队列,这可能导致竞态条件(race
condition)和数据不一致性。
以下是一些导致 std::queue
不适用于多线程环境的主要原因:
缺乏同步机制:
std::queue
不包含内建的同步机制,比如互斥量或锁。在多线程环境下,当多个线程同时尝试访问或修改队列时,没有适当的同步措施可能导致数据竞争。不提供原子操作:
std::queue
不提供原子操作,即使是简单的push
和pop
操作,也不能在多线程环境中保证原子性。多个线程可能同时执行这些操作,而没有合适的同步机制,可能导致不一致的队列状态。
1 |
|
消费者线程
cv.wait_for(lock, std::chrono::milliseconds(_connectionTimeout))
是等待条件变为真或超时的操作。它会在等待的过程中释放锁,允许其他线程在这段时间内访问共享资源。等待的时间由_connectionTimeout
指定,单位是毫秒。整个
if
语句检查条件变量的等待状态是否是超时状态。如果等待超时,表示在指定的时间内条件变为真的情况未发生。1
if (cv_status::timeout == cv.wait_for(lock, chrono::milliseconds(_connectionTimeout)))
这里如果写成下面的语句:
1
if (cv.wait_for(lock, chrono::milliseconds(_connectionTimeout)))
又可能出现并不是超时唤醒,而是被其它的进程唤醒了的问题。
1 | // 给外部提供接口,从连接池中获取一个可用的空闲连接 |
利用智能指针实现给外部提供接口
如果是给外界返回一个指针,还得实现一个把连接归还给连接池的过程比较复杂。
1 | Conection* getConnection(); |
可以返回一个智能指针,智能指针出作用域后会自动析构,我们可以重定义一些删除器让其在析构的时候把连接归还给连接池
1 |
|
1 | shared_ptr<Connection> sp(_connectionQue.front(), |
std::shared_ptr<Connection>
是一个智能指针,用于管理Connection
类型的对象。这里使用_connectionQue.front()
获取队列头部的Connection
指针作为被shared_ptr
所管理的对象。第二个参数是一个自定义的删除器,使用了 lambda 表达式
([&](Connection *pcon) {...})
。这个删除器会在shared_ptr
管理的对象引用计数变为零时被调用,用于自定义对象的销毁行为。- 在这个例子中,当
shared_ptr
的引用计数变为零时(没有任何shared_ptr
持有这个对象时),lambda 表达式会被调用。 - 在 lambda 表达式内部,首先通过互斥量
_queueMutex
对队列进行锁定(std::unique_lock<std::mutex>lock(_queueMutex);
)。这是因为涉及到队列的操作,需要考虑线程安全。 - 然后调用
pcon->refreshAliveTime()
刷新Connection
对象的空闲时间。 - 最后,将
pcon
重新放入队列中_connectionQue.push(pcon)
。
- 在这个例子中,当
这样,通过自定义删除器,你可以在 Connection
对象被释放时执行一些额外的操作。在这个例子中,是刷新空闲时间并将连接重新放入队列。这通常用于对象的资源管理和回收。
lambda表达式部分解释
[&]
: 这是 lambda 表达式的捕获列表(capture list),用于指定在 lambda 函数体内可访问的外部变量。[&]
表示以引用方式捕获所有外部变量,即在 lambda 函数体内,可以访问包含这个 lambda 的函数作用域内的所有变量,并且可以修改这些变量的值。(Connection *pcon)
: 这是 lambda 表达式的参数列表,表示 lambda 函数接受一个名为pcon
,类型为Connection*
的参数。{ /* ... */ }
: 这是 lambda 表达式的函数体,包含了 lambda 函数要执行的代码块。
综合起来,
[&](Connection *pcon) { /* ... */ }
这个 lambda 表达式表示一个接受一个Connection*
类型的参数pcon
的函数,它以引用方式捕获了包含它的作用域内的所有变量,并在函数体内执行一些操作。
最大空闲时间回收连接扫描线程
时间的记录
1 | // 刷新一下连接的起始的空闲时间点 |
定时线程
在构造函数里面进行线程的初始化:
1 | // 启动一个新的定时线程,扫描超过maxIdleTime时间的空闲连接,进行对于的连接回收 |
scannerConnectionTask
1 | // 扫描超过maxIdleTime时间的空闲连接,进行对于的连接回收 |