关键技术点
- MySQL数据库编程
- 单例模式
- queue队列容器
- C++11多线程编程、线程互斥、线程同步通信和
unique_lock - 基于CAS的原子整形、智能指针shared_ptr
- lambda表达式、生产者-消费者线程模型
项目背景
为了提高MySQL的访问瓶颈,除了在服务端缓存常用数据之外,还可以利用连接池来提高MySQL的访问效率。在高并发情况下,大量的TCP三次握手、MySQL Server连接认证、MySQL Server关闭连接回收资源和TCP四次挥手导致性能变差,增加连接池可以减少这部分的的性能损耗。
连接池主要功能点
连接池一般包含了数据库连接所用的ip、port、username、password、及其他性能参数,例如初始连接量、最大连接量、最大空闲时间、连接超时时间等,连接池主要就是实现上述四大通用基础功能。
- 初始连接量(initSize): 表示连接池事先会和MySQL Server创建initSize个数的connection连接,当
应用发起MySQL访问时,不用再创建和MySQL Server新的连接,直接从连接池中获取一个可用的连接
就可以,使用完成后,并不去释放connection,而是把当前connection再归还到连接池当中。 - 最大连接量(maxSize): 当并发访问MySQL Server的请求增多时,初始连接量已经不够使用了,此
时会根据新的请求数量去创建更多的连接给应用去使用,但是新创建的连接数量上限是maxSize,不能
无限制的创建连接,因为每一个连接都会占用一个socket资源,一般连接池和服务器程序是部署在一台
主机上的,如果连接池占用过多的socket资源,那么服务器就不能接收太多的客户端请求了。当这些连
接使用完成后,再次归还到连接池当中来维护。 - 最大空闲时间(maxidleTime):当访问MySQL的并发请求多了以后,连接池里面的连接数量会动态
增加,上限是maxSize个,当这些连接用完再次归还到连接池当中。如果在指定的maxIdleTime里面,
这些新增加的连接都没有被再次使用过,那么新增加的这些连接资源就要被回收掉,只需要保持初始连
接量initSize个连接就可以了。 - 连接超时时间(connectionTimeout):当访问MySQL的并发请求多了以后,连接池里面的连接数量会动态
增加,上限是maxSize个,当这些连接用完再次归还到连接池当中。如果在指定的maxIdleTime里面,
这些新增加的连接都没有被再次使用过,那么新增加的这些连接资源就要被回收掉,只需要保持初始连
接量initSize个连接就可以了。
功能实现设计
Connection类封装连接池
Connection类封装对数据库的操作
连接池主要包含了以下功能点:
1.连接池只需要一个实例,所以ConnectionPool以单例模式进行设计
2.从ConnectionPool中可以获取和MySQL的连接Connection
3.空闲连接Connection全部维护在一个线程安全的Connection队列中,使用线程互斥锁保证队列的线
程安全
4.如果Connection队列为空,还需要再获取连接,此时需要动态创建连接,上限数量是maxSize
5.队列中空闲连接时间超过maxIdleTime的就要被释放掉,只保留初始的initSize个连接就可以了,这个
功能点肯定需要放在独立的线程中去做
6.如果Connection队列为空,而此时连接的数量已达上限maxSize,那么等待connectionTimeout时间
如果还获取不到空闲的连接,那么获取连接失败,此处从Connection队列获取空闲连接,可以使用带
超时时间的mutex互斥锁来实现连接超时时间
7.用户获取的连接用shared_ptr智能指针来管理,用lambda表达式定制连接释放的功能(不真正释放
连接,而是把连接归还到连接池中)
8.连接的生产和连接的消费采用生产者-消费者线程模型来设计,使用了线程间的同步通信机制条件变量
和互斥锁
核心代码
公共类public
#pragma once
// 日志函数
#define Log(str) \
std::cout << __FILE__ << ":" << __LINE__ << " " << \
__TIMESTAMP__ << " : " << str << std::endl;
Connection类
自行拆分为头文件和源文件
/*
实现MySQL数据库操作
*/
#include <mysql.h>
#include <public.h>
#include <string>
using namespace std;
class Connection
{
public:
Connection(){
_conn = mysql_init(nullptr);
}
~Connection(){
if (_conn){
mysql_close(_conn);
}
}
// 连接数据库
bool connect(string ip, unsigned int 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);
return p;
}
// 更新(insert update delete)
bool update(string sql) {
if (mysql_query(_conn, sql.c_str())) {
LOG("更新失败"+sql);
return false;
}
return true;
}
MYSQL_RES* query(string sql) {
if (mysql_query(_conn, sql.c_str())) {
LOG("查询失败"+sql);
return nullptr;
}
return mysql_use_result(_conn);
}
void printQueryResult(MYSQL_RES* result) {
if (!result) {
std::cerr << "结果集为空" << std::endl;
return;
}
MYSQL_ROW row;
unsigned int num_fields = mysql_num_fields(result);
MYSQL_FIELD* field = mysql_fetch_fields(result);
// 打印表头
for (unsigned int i = 0; i < num_fields; i++) {
std::cout << fields[i].name << "\t";
}
std::cout << std::endl;
// 打印每一行的数据
while ((row = mysql_fetch_row(result))) {
for (unsigned int i = 0; i < num_fields; i++) {
std::cout <<(row[i] ? row[i] : "NULL") << "\t";
}
std::cout << std::endl;
}
// 释放结果集
mysql_free_result(result);
}
private:
MYSQL *_conn; //标识一条和Server的连接
}
ConnectionPool类
1. 首先创建一个单例模式的类
class ConnectionPool {
public:
static ConnectionPool* getConnectionPool();
private:
ConnectionPool();
~ConnectionPool();
string _ip; // ip地址
unsigned int _port; // 端口
string _username;
string _password;
string _dbname;
int _initSize; // 初始连接数
int _maxSize; // 最大连接数
int _maxIdleTime; // 最大空闲时间
int _connectionTimeout; // 超时时间
}
ConnectionPool *ConnectionPool::getConnectionPool() {
static ConnectionPool pool;
return &pool;
}
2.解析配置文件
# mysql配置文件
ip=127.0.0.1
port=3306
username=root
password=1
dbname=chat
initSize=10
maxSize=1024
#最大空闲时间单位是s
maxIdleTime=60
#连接超时时间单位ms
maxConnectionTimeout=100
bool ConnectionPool::loadConfigFile(const string& filename){
ifstream infile(filename.c_str());
if (!infile) {
LOG(filename + "is not exist!");
return false;
}
string line;
while(getline(infile, line)) {
if(line.empty()) {
continue;
}
size_t pos = line.find('=');
if (pos == string::npos) { // 无效配置
continue;
}
string key = line.substr(0, pos);
string value = line.substr(pos+1);
if (key == "ip") {
_ip = value;
} else if (key == "port"){
_port = stoul(value);
} else if (key == "username") {
_username = value;
} else if (key == "password") {
_password = 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 == "maxConnectionTimeout") {
_connectionTimeout = stoi(value);
}
}
return true;
}
3. 加载配置文件并且进行初始化
在构造函数中进行
ConnectionPool::ConnectionPool() {
// 加载配置项
if (!loadConfigFile("config/mysql.ini")) {
return ;
}
// 创建初始数量链接
for (int i = 0; i < _initSize; ++i) {
Connection *p = new Connection();
p->connect(_ip, _port, _username, _password, _dbname);
_connectionQue.push(p);
++_connectionCnt;
}
}
4. 设置一个子线程来监控这些连接
连接不够就增加,多了就减少
ConnectionPool::ConnectionPool() {
.....
// 启动一个新的线程,作为连接的生产者
//thread produce(&ConnectionPool::produceConnectionTask, this);
thread produce(std::bind(&ConnectionPool::produceConnectionTask, this));
}
void ConnectionPool::produceConnectionTask() {
for (;;) {
// 操作队列需要加锁
unique_lock<mutex> lock(_queueMutex);
while (!_connectionQue.empty()){
// 暂时解锁由 lock 持有的互斥锁。
//将当前线程置为等待状态,直到其他线程调用 _cv.notify_one() 或 _cv.notify_all() 来唤醒等待中的一个或所有线程。
//当线程被唤醒后,wait 会重新获取互斥锁并继续执行。
_cv.wait(lock); //队列不空,生产线程处于等待状态
}
// 连接未达上限,继续创建
if (_connectionCnt < _maxSize) {
Connection *p = new Connection();
p->connect(_ip, _port, _username, _password, _dbname);
_connectionQue.push(p);
++_connectionCnt;
}
// 通知消费者线程消费
_cv.notify_one();
}
}
5. 设置消费者
shared_ptr<Connection> ConnectionPool::getConnection() {
unique_lock<mutex> lock(_queueMutex);
if (_connectionQue.empty()) {
_cv.wait_for(lock, chrono::milliseconds(_connectionTimeout));
if (_connectionQue.empty()) {
LOG("获取空闲连接超时");
return nullptr;
}
}
shared_ptr<Connection> sp(_connectionQue.front());
_connectionQue.pop();
if (_connectionQue.empty()) {
// 消费完发现空了,及时通知生产
// 谁消费了最后一个,谁最进行通知
// 不加也可以,因为生产者会判断队列是否空了,进而跳过wait等待
_cv.notify_all();
}
return sp;
}
6. 重定义消费者使用完之后的析构
智能指针出作用域自动释放,默认会delete,我们需要重写让他放回池子, 直接给构造传第二个参数
shared_ptr<Connection> ConnectionPool::getConnection() {
...
shared_ptr<Connection> sp(_connectionQue.front(), [&](Connection *pCon){
unique_lock<mutex> lock(_queueMutex);
_connectionQue.push(pCon);
});
...
}
7. 空闲时间处理
将连接放入连接池和归还时都要进行刷新
新扫描建线程
void ConnectionPool::scannerConnectionTask() {
for (;;) {
this_thread::sleep_for(chrono::seconds(_maxIdleTime));
// 扫描队列释放多余连接
unique_lock<mutex> lock(_queueMutex);
while (_connectionCnt > _initSize) {
// 队头空闲时间小于60,后边也不用看了
Connection *p = _connectionQue.front();
if (p->getAliveTime() >= (_maxIdleTime * 1000)) {
_connectionQue.pop();
delete p;
} else {
break;
}
}
}
}
测试
建表
create DATABASE chat;
USE chat;
CREATE TABLE user (
id INT AUTO_INCREMENT PRIMARY KEY, -- 添加一个自增的主键
name VARCHAR(50) NOT NULL,
age TINYINT UNSIGNED NOT NULL CHECK (age >= 0 AND age <= 120), -- 确保年龄合理
sex ENUM('male', 'female') DEFAULT 'male' -- 设置默认值
);
#include <iostream>
#include <sstream>
#include <thread>
#include <ctime>
#include "./include/ConnectionPool.h"
int main() {
clock_t begin = clock();
// for (int i = 0; i < 1000; i++) {
// 单线程未使用线程池,2708ms
// Connection conn;
// const char *sql = "INSERT INTO `user` (`name`, `age`, `sex`) VALUES ('zhang san', 20, 'male')";
// if (conn.connect("127.0.0.1", 3306, "root", "123456", "chat") == false) {
// LOG("连接失败");
// }
// conn.update(sql);
//单线程使用线程池 579ms
// ConnectionPool *cp = ConnectionPool::getConnectionPool();
// shared_ptr<Connection> sp = cp->getConnection();
// string sql = "INSERT INTO `user` (`name`, `age`, `sex`) VALUES ('zhang san', 20, 'male')";
// sp->update(sql);
// }
// 三线程 942ms
thread t1([]() {
ConnectionPool *cp = ConnectionPool::getConnectionPool();
for (int i = 0; i < 1000; i++) {
shared_ptr<Connection> sp = cp->getConnection();
string sql = "INSERT INTO `user` (`name`, `age`, `sex`) VALUES ('zhang san', 20, 'male')";
sp->update(sql);
}
});
thread t2([]() {
ConnectionPool *cp = ConnectionPool::getConnectionPool();
for (int i = 0; i < 1000; i++) {
shared_ptr<Connection> sp = cp->getConnection();
string sql = "INSERT INTO `user` (`name`, `age`, `sex`) VALUES ('zhang san', 20, 'male')";
sp->update(sql);
}
});
thread t3([]() {
ConnectionPool *cp = ConnectionPool::getConnectionPool();
for (int i = 0; i < 1000; i++) {
shared_ptr<Connection> sp = cp->getConnection();
string sql = "INSERT INTO `user` (`name`, `age`, `sex`) VALUES ('zhang san', 20, 'male')";
sp->update(sql);
}
});
t1.join();
t2.join();
t3.join();
clock_t end = clock();
cout << (end - begin) << "ms" << endl;
return 0;
}