目的:
因为对数据库的操作实质上是对磁盘的IO操作,所以如果对数据库访问次数过多,就会到导致大量的磁盘IO,为了提高MySQL数据库(基于C/S设计)的访问瓶颈,除了在服务器端增加缓存服务器缓存常用的数据 之外(例如Redis),还可以增加连接池,来提高MySQL Server的访问效率。
在高并发情况下,大量的 TCP三次握手、MySQL Server连接认证、MySQL Server关闭连接回收资源和TCP四次挥手所耗费的时间也十分明显,增加连接池就是为了减少这一部分的性能损耗。
在系统启动时就创建一定数量的连接,用户一旦执行CURD操作,直接拿出一条连接即可,不需要TCP的连接过程和资源回收过程,使用完该连接后归还给连接池的连接队列,供之后使用。
功能介绍:
1、初始连接数(initSize):
初始连接量表示连接池事先会和MySQL Server创建的initSize数量的SqlConn连接。在完成初始连接量之后,当应用发起MySQL访问时,不用创建新的MySQLServer连接,而是从连接池中直接获取一个连接,当使用完成后,再把连接归还到连接池中。
2、最大容量(maxCapacity)
连接池的最大容量即连接池内的最大连接数。当并发访问MySQL Server的请求增多时,初始连接量不够使用时,会根据新的请求数量去创建更多的连接给应用去使用,但是新创建的连接数量上限是maxCapacity,不能无限制的创建连接,因为每一个连接都会占用一个socket资源,一般连接池和服务器程序是部署在一台 主机上的,如果连接池占用过多的socket资源,那么服务器就不能接收太多的客户端请求了。当这些连接使用完成后,会再次归还到连接池当中来维护。
3、最大空闲时间(maxIdleTime)
当高并发过去,因为高并发而新创建的连接在很长时间(maxIdleTime)内没有得到使用,那么这些新创建的连接处于空闲状态,并且占用着一定的资源,这个时候就需要将其释放掉,最终只用保存initSize个连接就行。
4、连接超时时间(connTimeOut)
当MySQL的并发访问请求量过大,连接池中的连接数量已经达到了maxSize,并且此时连接池中没有可以使用的连接,那么此时应阻塞connTimeOut的时间,如果此时间内有使用完的连接归还到连接池,那么就可以使用,如果超过这个时间还是没有连接,那么获取数据库连接就失败,无法访问数据库。
运行流程:
1、使用单例模式(局部静态变量方法)获取连接池实例。
SqlConnPool* pool = SqlConnPool::getInstance();
2、从SqlConnPool中获取和Mysql的连接SqlConn,为了实现RAII的目标,将getConn()函数的返回值设置为shared_ptr<SqlConn>类型。
shared_ptr<SqlConn> sp = pool->getConn();
3、利用获取的连接,对数据库进行CURD操作。
sp->update(sql);
sp->query(sql);
SqlConn类结构 SqlConn.h
#ifndef SQLCONN_H
#define SQLCONN_H
#include <mysql/mysql.h>
#include <chrono>
#include <string>
class SqlConn
{
public:
SqlConn();
~SqlConn();
//建立Sql连接
bool connect(const std::string& ip,
const uint16_t port,
const std::string& user,
const std::string& pwd,
const std::string& dbName);
// 更新操作 insert、delete、update
bool update(const std::string& sql);
// 查询操作 select
MYSQL_RES* query(const std::string& sql);
//刷新存活时间
void refreshAliveTime()
{
aliveTime = std::chrono::steady_clock::now();
}
//获取存活时间间隔
long long getAliveTime()
{
return std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - aliveTime).count();
}
private:
MYSQL* conn;//数据库连接
std::chrono::time_point<std::chrono::steady_clock> aliveTime; //连接存活时间
};
#endif // !SQLCONN_H
Sqlconn实现 Sqlconn.cpp
#include "sqlconn.h"
SqlConn::SqlConn()
{
conn=mysql_init(nullptr);
mysql_set_character_set(conn, "utf8");
}
SqlConn::~SqlConn()
{
if(conn!=nullptr)
{
mysql_close(conn);
}
}
bool SqlConn::connect(const std::string& ip,const uint16_t port,const std::string& user,const std::string& pwd,const std::string& dbName)
{
MYSQL* ptr=mysql_real_connect(conn,ip.c_str(),user.c_str(),pwd.c_str(),dbName.c_str(),port,nullptr,0);
return ptr!=nullptr;
}
bool SqlConn::update(const std::string& sql)
{
if(mysql_query(conn,sql.c_str()))
{
return false;
}
return true;
}
MYSQL_RES* SqlConn::query(const std::string& sql)
{
if(mysql_query(conn, sql.c_str()))
{
return nullptr;
}
return mysql_use_result(conn);
}
注:mysql_query()函数有两种返回值,如果执行的是更新操作(insert、delete、update),则返回 true/ false;如果执行的是查询操作(select),则返回 MYSQL_RES* 类型。
SqlConnPool类结构 SqlConnPool.h
#ifndef SQLCONNPOOL_H
#define SQLCONNPOOL_H
#include <mysql/mysql.h>
#include <memory>
#include <string>
#include "sqlconn.h"
#include "blockqueue.h"
#include "log.h"
class SqlConnPool
{
public:
static SqlConnPool* getInstance(int maxCapacity=1024)
{
static SqlConnPool single(maxCapacity);
return &single;
}
std::shared_ptr<SqlConn> getConn();
private:
SqlConnPool(int maxCapacity);
~SqlConnPool(){};
SqlConnPool(const SqlConnPool& other)=delete;
SqlConnPool& operator =(const SqlConnPool& other)=delete;
//加载配置文件
bool loadConfigFile();
//专门用于产生新连接的线程
void produceConnTask();
//扫描超过最大空闲时间maxIdleTime的连接,对其进行回收
void scannerConnTask();
//添加新连接
void addConn();
std::string ip;
uint16_t port;
std::string user;
std::string pwd;
std::string dbName;
size_t initSize;//初始连接数
size_t maxIdleTime;//最大空闲时间
size_t connTimeout;//超时时间
BlockQueue<SqlConn*> connQue;
};
#endif // !SQLCONNPOOL_H
SqlConnPool实现 SqlConnPool.cpp
#include "sqlconnpool.h"
#include <functional>
using std::string;
bool SqlConnPool::loadConfigFile()//加载mysql.ini配置文件
{
FILE* fp = fopen("./mysql.ini", "r");
if (fp == nullptr)
{
LOG_ERROR("%s","mysql.ini file dose not exsit!");
return false;
}
while(!feof(fp)) //循环读取配置文件的每一行
{
char line[1024] = {0};
fgets(line, 1024, fp);
string str = line;
int idx = str.find('=', 0);
if (idx == -1)
{
continue;
}
int endidx = str.find('\n', idx);
string key = str.substr(0, idx);//截取key
string value = str.substr(idx+1,endidx-idx-1);//截取value
if (key == "ip")
ip = value;
else if (key == "port")
port = stoi(value);
else if (key == "username")
user = value;
else if (key == "password")
pwd = value;
else if (key == "dbName")
dbName = value;
else if (key == "initSize")
initSize = stoi(value);
else if (key == "maxIdleTime")
maxIdleTime = stoi(value);
else if (key == "connTimeOut")
connTimeout = stoi(value);
}
return true;
}
void SqlConnPool::addConn()//添加Sql连接
{
SqlConn* conn=new SqlConn();
conn->connect(ip,port,user,pwd,dbName);
conn->refreshAliveTime();
connQue.push_back(conn);
}
//构造SqlConnPool,初始化connQue变量
SqlConnPool::SqlConnPool(int maxCapacity):connQue(maxCapacity)
{
if (!loadConfigFile())//配置失败
{
return ;
}
for (size_t i = 0; i < initSize; i++)//添加初始initSize数量的连接
{
addConn();
}
//生产者线程
std::thread produce(std::bind(&SqlConnPool::produceConnTask, this));
produce.detach();
//扫描线程
std::thread scanner(std::bind(&SqlConnPool::scannerConnTask, this));
scanner.detach();
}
void SqlConnPool::produceConnTask()
{
for(;;)
{
addConn();
}
}
void SqlConnPool::scannerConnTask()
{
for (;;)
{
std::this_thread::sleep_for(std::chrono::microseconds(maxIdleTime));
while (connQue.get_size() > initSize)//清除超出最大空闲时间的连接
{
SqlConn* p = connQue.front();
if(p->getAliveTime() >= maxIdleTime)
{
SqlConn* tmp=nullptr;
connQue.pop(tmp);
delete tmp;
}
else
break;
}
}
}
std::shared_ptr<SqlConn> SqlConnPool::getConn()//获取一个Sql连接
{
SqlConn* conn;
if (!connQue.pop(conn,connTimeout))//超过连接超时时间
{
LOG_ERROR("%s","get connection timeout");
return nullptr;
}
std::shared_ptr<SqlConn> sp(conn,
[&](SqlConn* p)
{
p->refreshAliveTime();
connQue.push_back(p);
});//自定义删除函数(删除时将连接再次放入阻塞队列中,重复使用)
return sp;
}
测试程序 testSqlConnPool.cpp
四种试验:多线程无连接池、多线程有连接池、单线程无连接池、单线程有连接池
分别测试其“建立10000个数据库连接,每个连接执行一次插入语句”的运行时间。
#include "sqlconn.h"
#include "sqlconnpool.h"
#include <time.h>
#include <iostream>
using namespace std;
void SigleWithConnection()//单线程有连接池
{
time_t begin = clock();
for (int i = 0; i < 10000; ++i)
{
SqlConnPool* pool = SqlConnPool::getInstance();
shared_ptr<SqlConn> sp = pool->getConn();
char sql[1024] = {0};
sprintf(sql, "insert into user(username,password) values('%s','%s')","zhangsan","123456");
sp->update(sql);
}
time_t end = clock();
cout << (end - begin)/1000 << " ms" << endl;
}
void SigleNoConnection()//单线程无连接池
{
time_t begin = clock();
for (int i = 0; i < 10000; ++i)
{
SqlConn conn;
char sql[1024] = {0};
sprintf(sql, "insert into user(username,password) values('%s','%s')","zhangsan","123456");
conn.connect("127.0.0.1", 3306, "root", "123456", "yourdb");
conn.update(sql);
}
time_t end = clock();
cout << (end - begin)/1000 << " ms" << endl;
}
void runInThreadNoConn()
{
for (int i = 0; i < 2500; ++i)
{
SqlConn conn;
char sql[1024] = { 0 };
sprintf(sql, "insert into user(username,password) values('%s','%s')","zhangsan","123456");
conn.connect("127.0.0.1", 3306, "root", "123456", "yourdb");
conn.update(sql);
}
}
void MutiNoConnection()//多线程无连接池
{
SqlConn conn;
conn.connect("127.0.0.1", 3306, "root", "123456", "yourdb");
time_t begin = clock();
for(int i=0;i<4;i++)
{
thread t=thread(runInThreadNoConn);
t.join();
}
time_t end = clock();
cout << (end - begin)/1000 << " ms" << endl;
}
void runInThreadByConn()
{
for (int i = 0; i < 2500; i++)
{
SqlConnPool* pool = SqlConnPool::getInstance();
shared_ptr<SqlConn> sp = pool->getConn();
char sql[1024] = {0};
sprintf(sql, "insert into user(username,password) values('%s','%s')","zhangsan","123456");
if (sp == nullptr)
{
cout << "sp is empty" << endl;
continue;
}
sp->update(sql);
}
}
void MutiWithConnection()//多线程有连接池
{
time_t begin = clock();
for(int i=0;i<4;i++)
{
thread t=thread(runInThreadByConn);
t.join();
}
time_t end = clock();
cout << (end - begin)/1000 << " ms" << endl;
}
int main()
{
MutiNoConnection();//多线程无连接池
MutiWithConnection();//多线程有连接池
SigleNoConnection();//单线程无连接池
SigleWithConnection();//单线程有连接池
return 0;
}
测试结果
由结果可知,使用多线程+连接池的组合有效提高了数据库的访问效率。
- 数据量10000,单线程从5280ms变成2080ms
- 数据量10000,多线程从2140ms变成了1910ms
附:
配置文件 mysql.ini
# 数据库连接池配置文件
ip=127.0.0.1
port=3306
username=root
password=123456
dbName=yourdb
#初始连接数
initSize=4
# 最大空闲时间,单位ms
maxIdleTime=6000
# 连接超时时间,单位ms
connTimeOut=100
Makefile
CXX = g++
CFLAGS = -std=c++14 -O2 -Wall -g
TARGET = testSqlConnPool
OBJS = buffer.cpp log.cpp blockqueue.h\
sqlconn.cpp sqlconnpool.cpp\
testSqlConnPool.cpp
all: $(OBJS)
$(CXX) $(CFLAGS) $(OBJS) -o $(TARGET) -pthread -L/usr/lib64/mysql -lmysqlclient
clean:
rm -rf $(OBJS) $(TARGET)
阻塞队列实现
基于C++11实现的阻塞队列(BlockQueue)_{(sunburst)}的博客-CSDN博客
删去代码中的LOG_XXXX语句,就无需链接 log.cpp,且无需链接仅与日志系统有关的 buffer.cpp
日志系统实现
同步+异步日志系统(C++实现)_{(sunburst)}的博客-CSDN博客
缓冲区实现
缓冲区Buffer类的设计(参考Muduo实现)_{(sunburst)}的博客-CSDN博客