项目背景
在高并发情况下,如果每次用户访问数据库都需要创建和销毁数据库连接对象,这将导致大量的TCP三次握手、MySQL Server连接认证、MySQL Server关闭连接以及资源回收的操作,这些过程会带来巨大的性能开销。如果在一开始就创建好一些连接对象存放在连接池中,当用户需要时从连接池中取出,用完时归还,就可以起到资源重用,从而节省了频繁创建连接销毁连接所花费的时间,提升系统响应的速度。
连接池功能
连接池设计

关键技术点
- MySQL数据库编程
- 单例模式
- C++11多线程编程、生产者-消费者线程模型
- 智能指针shared_ptr、lambda表达式
项目配置
使用vs2019编写项目代码,由于需要调用MySQL API来完成数据库的连接、查询和更新等操作,需要在vs2019中导入MySQL的头文件、库文件。
VS2019选择X64,因为安装的MySQL是64位的

右键项目 - 属性 - C/C++ - 常规 - 附加包含目录,填写mysql.h头文件的路径(例如:D:\developer_tools\MySQL\Files\MySQL Server 5.7\include)。注意:先在源文件下创建一个.cpp文件后,下图中C/C++的选项才会出现。

右键项目 - 属性 - 链接器 - 常规 - 附加库目录,填写libmysql.lib的路径(D:\developer_tools\MySQL\Files\MySQL Server 5.7\lib)
右键项目 - 属性 - 链接器 - 输入 - 附加依赖项,填写libmysql.lib库的名字(libmysql.lib)(静态库)
把libmysql.dll动态链接库(Linux下后缀名是.so库)放在工程目录下(动态库)

数据库编程
创建数据表
1 2 3 4 5 6
| create databases pool; use pool; CREATE TABLE user(id INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT, name VARCHAR(50) DEFAULT NULL, age INT(11) DEFAULT NULL, sex ENUM('male','female') DEFAULT NULL)ENGINE=INNODB DEFAULT CHARSET=utf8;
SQL
|

MySQL C API
需要包含头文件#include <mysql.h>
初始化MySQL连接
1 2
| MYSQL *mysql_init(MYSQL *mysql) ;
C
|
连接MySQL服务器
1 2 3 4 5 6 7 8 9
| MYSQL * STDCALL mysql_real_connect(MYSQL *mysql, const char *host, const char *user, const char *passwd, const char *db, unsigned int port, const char *unix_socket, unsigned long clientflag);
C
|
执行sql语句
1 2 3 4 5 6 7 8
| int mysql_query(MYSQL *mysql, const char *query); 参数: - mysql: mysql_real_connect()的返回值 - query: 一个可以执行的sql语句, 结尾的位置不需要加';' 返回值: - 如果查询成功,返回0。如果是查询, 结果集在mysql对象中 - 如果出现错误,返回非0值。
C
|
获取结果集
1
| MYSQL_RES *mysql_use_result(_conn);
SCSS
|
关闭MySQL
使用C++封装MySQL C API
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
| #ifndef CONNECTION_H #define CONNECTION_H #include <mysql.h> #include <string> #include <ctime> #include "public.h" using namespace std; class Connection { public: Connection();
~Connection();
bool connect(string ip, unsigned short port, string user, string passward, string dbname);
bool update(string sql);
MYSQL_RES* query(string sql); private: MYSQL* conn_; }; #endif
C++
|
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
| #include "Connection.h"
Connection::Connection() { conn_ = mysql_init(nullptr); }
Connection::~Connection() { if (conn_ != nullptr) mysql_close(conn_); }
bool Connection::connect(string ip, unsigned short port, string user, string password, string dbname) { MYSQL* p = mysql_real_connect(conn_, ip.c_str(), user.c_str(), password.c_str(), dbname.c_str(), port, nullptr, 0); cerr << mysql_error(conn_) << endl; return p != nullptr; }
bool Connection::update(string sql) { if (mysql_query(conn_, sql.c_str())) { LOG("Update Failed:" + sql); return false; } return true; }
MYSQL_RES* Connection::query(string sql) { if (mysql_query(conn_, sql.c_str())){ LOG("Query Failed:" + sql); return nullptr; } return mysql_use_result(conn_); }
CPP
|
测试一下效果:
1 2 3 4 5 6 7 8 9 10
| #include "Connection.h" #include <iostream> using namespace std; int main() { Connection conn; string sql = "insert into user(name,age,sex) values('li', 21, 'female')"; conn.connect("127.0.0.1", 13306, "root", "123456", "pool"); conn.update(sql); return 0; }
C++
|
可以看到,成功插入一条新数据:

功能模块实现
线程安全的懒汉式单例模式
连接池只需要一个实例,所以ConnectionPool应该按照单例模式设计,即一个类不管创建多少对象,永远只能得到该类型的一个对象实例。单例模式又分为懒汉式单例模式和饿汉式单例模式。相比于懒汉式单例模式,饿汉式单例模式在没有需要获取实例对象前,实例对象就已经产生了,这样会浪费系统资源使启动时间过长。因此,在数据库连接池的设计中,我们使用线程安全的懒汉式单例模式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #ifndef CONNECTIONPOOL_H #define CONNECTIONPOOL_H #include "Connection.h" class ConnectionPool { public: static ConnectionPool* getConnectionPool(); private: ConnectionPool(); }; #endif
#include "ConnectionPool.h"
ConnectionPool* ConnectionPool::getConnectionPool() { static ConnectionPool pool; return &pool; }
C++
|
从配置文件加载配置项
- 打开配置文件
- 判断文件是否存在
- 循环按行读取记录,按=和\n分割提取key和value
- 给私有成员赋值
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 52 53 54
| bool ConnectionPool::loadConfigFile() { ifstream ifs; ifs.open("mysql.ini", ios::in); if (!ifs.is_open()) { LOG("mysql.ini file is not exist!"); return false; }
string line; while (std::getline(ifs, line)) { string str = line; int idx = str.find('=', 0); if (idx == -1) { continue; } int endIdx = str.find('\n', idx); string key = str.substr(0, idx); string value = str.substr(idx + 1, endIdx - idx - 1); if (key == "ip") { ip_ = value; } else if (key == "port") { port_ = stoi(value); } else if (key == "username") { username_ = value; } else if (key == "passward") { passward_ = value; } else if (key == "dbname") { dbname_ = value; } else if (key == "initSize") { initSize_ = stoi(value); } else if (key == "maxSize") { maxSize_ = stoi(value); } else if (key == "maxIdleTime") { maxIdleTime_ = stoi(value); } else if (key == "connectionTimeout") { connectionTimeout_ = stoi(value); } } ifs.close(); return true; }
C++
|
连接池的构造
- 加载配置项
- 创建初始数量的连接
- 启动连接池的生产者线程
- 启动连接池定时清理线程
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
| #ifndef CONNECTIONPOOL_H #define CONNECTIONPOOL_H #include "Connection.h" #include <fstream> #include <string> #include <mutex> #include <queue>
class ConnectionPool { public: static ConnectionPool* getConnectionPool(); private: ConnectionPool();
bool loadConfigFile();
void produceConnectionTask();
void scannerConnectionTask();
string ip_; unsigned int port_; string username_; string passward_; string dbname_; int initSize_; int maxSize_; int maxIdleTime_; int connectionTimeout_;
queue<Connection*> connectionQue_; mutex queueMutex_; atomic_int connectionCnt_; condition_variable cv; }; #endif
C++
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| ConnectionPool::ConnectionPool() { if (!loadConfigFile()) { return; }
for (int i = 0; i < initSize_; i++) { Connection* p = new Connection(); p->connect(ip_, port_, username_, passward_, dbname_); connectionQue_.push(p); connectionCnt_++; }
thread produce(bind(&ConnectionPool::produceConnectionTask, this)); produce.detach();
thread scanner(bind(&ConnectionPool::scannerConnectionTask, this)); scanner.detach(); }
C++
|
生产者线程
作用:生产连接
- 作为类的成员方法,可以很方便地访问类的成员变量
- 由于线程函数是C接口的方法,因此传递函数时需要用绑定器将this指针绑定到生产者线程上
- 加锁
- 连接池队列不空时,进入等待状态
- 连接池队列为空时,生产连接
- 通知消费者线程
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
| thread produce(bind(&ConnectionPool::produceConnectionTask, this)); produce.detach();
void ConnectionPool::produceConnectionTask() { for (;;) { unique_lock<mutex> lock(queueMutex_);
while (!connectionQue_.size()) cv.wait(lock);
if (connectionCnt_ < maxSize_) { Connection* p = new Connection(); p->connect(ip_, port_, username_, passward_, dbname_); connectionQue_.push(p); connectionCnt_++; }
cv.notify_all(); } }
C++
|
消费者线程
作用:提供给外部的接口,从连接池中获取一个可用的空闲连接。返回一个智能指针(封装了连接),因为智能指针能够出作用域自动析构,可以重定义其删除器归还连接,避免让用户自己调用函数将连接归还,简化用户操作
- 加锁
- 连接池队列为空时,进入等待状态,并判断是否连接超时
- 连接池队列不空时,消费连接
- 通知生产者线程
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
| shared_ptr<Connection> ConnectionPool::getConnection() { unique_lock<mutex> lock(queueMutex_);
while (connectionQue_.empty()) { cv_status status = cv.wait_for(lock, chrono::milliseconds(connectionTimeout_)); if (status == cv_status::timeout && connectionQue_.empty()) { LOG("获取空闲连接超时...获取连接失败!"); return nullptr; } }
shared_ptr<Connection> sp(connectionQue_.front(), [&](Connection* pcon) { unique_lock<mutex> lock(queueMutex_); connectionQue_.push(pcon); }); connectionQue_.pop();
if (connectionQue_.empty()) { cv.notify_all(); } return sp; }
C++
|
定时清理连接线程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| void ConnectionPool::scannerConnectionTask() { for (;;) { this_thread::sleep_for(chrono::seconds(maxIdleTime_));
unique_lock<mutex> lock(queueMutex_); while (!connectionQue_.empty()) { Connection* p = connectionQue_.front(); if (connectionCnt_ > initSize_ && p->getAlivetime() >= maxIdleTime_ * 1000) { connectionQue_.pop(); delete p; connectionCnt_--; } else { break; } } } }
C++
|
在Linux下编译数据库连接池
将源文件和头文件放在一个文件夹下,如下:
1 2 3 4 5 6 7 8
| . ├── Connection.cpp ├── Connection.h ├── ConnectionPool.cpp ├── ConnectionPool.h ├── main.cpp ├── mysql.ini └── public.h
STYLUS
|
在终端执行g++指令:
1 2 3 4 5 6
| cd ConnectionPool
g++ -o ConnectionPool Connection.cpp ConnectionPool.cpp main.cpp -I/usr/include/mysql -L/usr/lib/x86_64-linux-gnu -lmysqlclient -lpthread
AWK
|
执行:
1
| ./ConnectionPool
JBOSS-CLI
|
Linux动态库
动态库的工作原理
- 静态库:GCC 进行链接时,会把静态库中代码打包到可执行程序中
- 动态库:GCC 进行链接时,动态库的代码不会被打包到可执行程序中(会记录动态库的信息)
- 动态库在程序运行时动态载入内存,当程序用到了动态库的哪个api,动态载入器会去寻找动态库文件放到内存里面
- 可以通过ldd (list dynamic dependencies)命令检查动态库依赖关系
如何定位共享文件?
- 当系统加载可执行代码时候,能够知道其所依赖的库的名字,但是还需要知道绝对路径
- 系统的动态载入器来获取该绝对路径
- 对于elf格式的可执行程序,是由ld-linux.so(动态载入器)来完成的,它先后搜索elf文件的 DT_RPATH段(无法修改) ——> 环境变量LD_LIBRARY_PATH ——> /etc/ld.so.cache文件列表 ——> /lib/,/usr/lib目录找到库文件后将其载入内存。
数据库连接池动态库的生成
生成.o的二进制目标文件,得到与位置无关的代码:
1 2
| g++ -c -fPIC Connection.cpp ConnectionPool.cpp
R
|
生成动态库:
1
| g++ -shared Connection.o ConnectionPool.o -o libconpool.so
STYLUS
|
配置动态载入器加载路径:
1 2 3 4 5 6
| // 修改/etc/ld.so.conf sudo vim /etc/ld.so.conf // 添加路径 /home/ubuntu/workspace/lib // 使其生效 sudo ldconfig
AWK
|
编译链接程序:
1
| g++ -o conn main.cpp -L/home/ubuntu/workspace/lib -lconpool -pthread -I/usr/include/mysql -L/usr/lib/x86_64-linux-gnu -lmysqlclient -lpthread
AWK
|
运行:
压力测试
数据量 |
未使用连接池花费的时间 |
使用连接池花费的时间 |
1000 |
单线程:0.85s 四线程:0.73 |
单线程:0.06 四线程:0.04 |
5000 |
单线程:4.13s 四线程:3.72 |
单线程:0.23 四线程:0.16 |
10000 |
单线程:8.36s 四线程:7.56 |
单线程:0.44 四线程:0.3 |
遇到的问题
在windows下能够正常运行,但是在linux下连接服务器上数据库报错‘@’localhost’ (using password: YES) - Access denied for user ‘root。排查了密码和权限都没问题,最后发现是因为Linux和Unix使用的换行符是’\n’,而Windows使用的是’\r\n’。在windows下,std::getline() 函数默认会将\r\n视为换行符,因此它会读取整个行,但会忽略掉 \r。而在Linux中,std::getline 仅将\n视为换行符,因此在读取到换行符时,\r 会作为普通字符一起读入。
在加载配置文件函数中增加去除’/r’的逻辑:
1 2 3 4 5
| for(int i=0; i < line.size(); i++) { if(line[i]==' ' || line[i]=='\r') continue; str += line[i]; }
C++
|
参考资料