提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、连接池设计
-
- [1.1 功能设计](#1.1 功能设计)
- [1.2 封装设计](#1.2 封装设计)
- 二、连接数据库步骤
- 三、封装编码
-
- [3.1 VS配置](#3.1 VS配置)
-
- [3.1.1 MySQL环境](#3.1.1 MySQL环境)
- [3.1.2 jsoncpp环境](#3.1.2 jsoncpp环境)
- [3.2 连接类代码设计](#3.2 连接类代码设计)
-
- [3.2.1 连接类MysqlConn.h](#3.2.1 连接类MysqlConn.h)
- [3.2.1 连接类MysqlConn.cpp](#3.2.1 连接类MysqlConn.cpp)
- [3.3 连接池代码设计](#3.3 连接池代码设计)
-
- [3.3.1 连接池connectionPool.h](#3.3.1 连接池connectionPool.h)
- [3.3.2 连接池connectionPool.cpp](#3.3.2 连接池connectionPool.cpp)
- [3.4 测试函数](#3.4 测试函数)
- [四、 总结](#四、 总结)
- 问题总结
-
- [MySQL "error: 'fd': 未知重写说明符"](#MySQL “error: ‘fd’: 未知重写说明符”)
- [LNK2019 无法解析的外部符号 _mysql_set_character_set@8,](#LNK2019 无法解析的外部符号 _mysql_set_character_set@8,)
- [由于找不到libmysql.dIl, 无法继续执行代码。重新安装程序可能会解决此问题](#由于找不到libmysql.dIl, 无法继续执行代码。重新安装程序可能会解决此问题)
前言
在进行数据库操作的时候为了提高数据库(关系型数据库)的访问瓶颈,除了在服务器端增加缓存服务器(例如redis)缓存常用的数据之外,还可以增加连接池,来提高数据库服务器的访问效率。
一般来说,对于数据库操作都是在访问数据库的时候创建连接,访问完毕断开连接 。但是如果在高并发情况下,有些需要频繁处理的操作就会消耗很多的资源和时间,比如:
- 建立通信连接的TCP三次握手
- 数据库服务器的连接认证
- 数据库服务器关闭连接时的资源回收
- 断开通信连接的TCP四次挥手
如果使用数据库连接池会减少这一部分的性能损耗
编写代码需要的头文件:
#include<mysql.h>
#include<json/json.h>
#include<jsoncpp.h>
API对应的MySQL动态库
Windows:libmysql.dll
Linux:libmysqlclient.os
一、连接池设计
1.1 功能设计
要设计一个数据库连接池,我们需要实现以下几个功能点:
- 连接池只需要一个实例,所以连接池类应该是一个单例模式的类 (此为设计模式)
- 所有的数据库连接应该维护到一个安全的队列中
- 使用队列的目的是方便连接的添加和删除
- 安全指的是线程安全,也就是说需要使用互斥锁来保护队列数据的读写。
- 在需要的时候可以从连接池中得到一个或多个可用的数据库连接
- 如果有可用连接,直接取出
- 如果没有可用连接,阻塞等待一定时长然后再重试
- 如果队列中没有多余的可用连接,需要动态的创建新连接
- 如果队列中空闲的连接太多,需要动态的销毁一部分
- 数据库操作完毕,需要将连接归还到连接池中
1.2 封装设计
- 数据库连接的存储 :可用使用STL中的队列
queue
- 连接池连接的动态创建:这部分工作需要交给一个单独的线程来处理
- 连接池连接的动态销毁:这部分工作需要交给一个单独的线程来处理
- 数据库连接的添加和归还 :这是一个典型的生产者和消费者模型
- 消费者:需要访问数据库的线程,数据库连接被取出(消费)
- 生产者:专门负责创建数据库连接的线程
- 处理生产者和消费者模型需要使用条件变量阻塞线程
- 连接池的默认连接数量:连接池中提供的可用连接的最小数量
- 如果不够就动态创建
- 如果太多就动态销毁
- 连接池的最大连接数量:能够创建的最大有效数据库连接上限
- 最大空闲时间:创建出的数据库连接在指定时间长度内一直未被使用,此时就需要销毁该连接。
- 连接超时:消费者线程无法获取到可用连接是,阻塞等待的时间长度
综上所述,数据库连接池对应的单例模式的类的设计如下:
cpp
using namespace std;
/*
* 数据库连接池: 单例模式
* MySqlConn 是一个连接MySQL数据库的类
*/
class ConnectionPool {
public:
// 得到单例对象
static ConnectionPool* getConnectPool();
// 从连接池中取出一个连接
shared_ptr<MySqlConn> getConnection();
// 删除拷贝构造和拷贝赋值运算符重载函数
ConnectionPool(const ConnectionPool& obj) = delete;
ConnectionPool& operator=(const ConnectionPool& obj) = delete;
private:
// 构造函数私有化
ConnectionPool();
bool parseJsonFile();
void produceConnection();
void recycleConnection();
void addConnection();
string m_ip; // 数据库服务器ip地址
string m_user; // 数据库服务器用户名
string m_dbName; // 数据库服务器的数据库名
string m_passwd; // 数据库服务器密码
unsigned short m_port; // 数据库服务器绑定的端口
int m_minSize; // 连接池维护的最小连接数
int m_maxSize; // 连接池维护的最大连接数
int m_maxIdleTime; // 连接池中连接的最大空闲时长
int m_timeout; // 连接池获取连接的超时时长
queue<MySqlConn*> m_connectionQ; // 连接队列
mutex m_mutexQ; // 互斥锁
condition_variable m_cond; // 条件变量
};
二、连接数据库步骤
在程序中连接MySql服务器,主要分为已经几个步骤:
- 初始化连接环境
- 连接mysql的服务器,需要提供如下连接数据:
- 服务器的IP地址 服务器监听的端口(默认端口是3306)
- 连接服务器使用的用户名(默认是 root),和这个用户对应的密码
- 要操作的数据库的名字
-
连接已经建立, 后续操作就是对数据库数据的添删查改
-
如果要进行数据 添加/ 删除/ 更新,需要进行事务的处理
- 需要对执行的结果进行判断
- 成功:提交事务
- 失败:数据回滚
- 需要对执行的结果进行判断
-
数据库的读操作 -> 查询 -> 得到结果集
-
遍历结果集 -> 得到了要查询的数据
-
释放资源
三、封装编码
3.1 VS配置
3.1.1 MySQL环境
打开项目的属性窗口,在属性页配置MySQL头文件目录和MySQL的库目录
将下载的mysql路径下的include和lib文件目录分别写入包含目录和库目录中
在VS项目的属性页窗口指定要加载的动态库对应的导入库(xxx.lib)
我们在调用MySQL API的使用需要加载的动态库为libmysql.dll
,它对应的导入库为libmysql.lib
,在该窗口的附加依赖项位置指定的就是这个导入库的名字。
3.1.2 jsoncpp环境
参考博客:jsoncpp的编译和使用
3.2 连接类代码设计
3.2.1 连接类MysqlConn.h
cpp
#pragma once
#include <string>
#include <WinSock2.h>//必须要有,解决"fd":未知重写说明符
#include <algorithm>
#include<chrono>
#include<mysql.h>
using namespace std;
using namespace chrono;
class MysqlConn
{
public:
// 初始化数据库连接
MysqlConn();
// 释放数据库连接
~MysqlConn();
// 连接数据库
bool connect(string userName,string passwd,string dbName,string ip,unsigned short port=3306);
// 更新数据库
bool update(string sql);
// 查询数据库
bool query(string sql);
// 遍历查询得到的结果集
bool next();
// 得到结果集中的字段值
string value(int index);
// 事务操作
bool transaction();
// 提交事务
bool commit();
// 事务回滚
bool rollback();
// 刷新起始的空闲时间点
void refreshAliveTime();
// 计算连接存活的时长
long long getAliveTime();
private:
void freeResource(); // 释放资源
MYSQL* m_conn = nullptr; // 保存初始化数据库时返回的地址
MYSQL_RES* m_result = nullptr; // 保存查询结果
MYSQL_ROW m_row = nullptr; // 保存当前记录中每个字段的值
steady_clock::time_point m_alivetime; // 存活时间
};
3.2.1 连接类MysqlConn.cpp
cpp
#include "MysqlConn.h"
// 初始化数据库连接
MysqlConn::MysqlConn()
{
// 初始化数据库
this->m_conn = mysql_init(nullptr);
// 设置接口的字符编码,防止在数据库操作时中文乱码
mysql_set_character_set(this->m_conn, "utf8");
}
// 释放数据库连接
MysqlConn::~MysqlConn()
{
// 释放内存
if (this->m_conn != nullptr) {
mysql_close(this->m_conn);
}
this->freeResource();
}
// 连接数据库
bool MysqlConn::connect(string userName, string passwd, string dbName, string ip, unsigned short port)
{
// c_str()函数返回一个指向正规C字符串的指针常量, 内容与本string串相同。
// 因为调用的MysqlAPI是C语言规范
MYSQL* ptr = mysql_real_connect(this->m_conn, ip.c_str(), userName.c_str(), passwd.c_str(), dbName.c_str(), port, nullptr, 0);
return ptr!=nullptr;
}
// 更新数据库
bool MysqlConn::update(string sql)
{
// 执行失败返回非0,执行成功返回0
if (mysql_query(this->m_conn,sql.c_str()))
{
return false;
}
return true;
}
// 查询数据库
bool MysqlConn::query(string sql)
{
// 先清空上次查询的数据结果
this->freeResource();
if (mysql_query(this->m_conn, sql.c_str()))
{
return false;
}
// 保存查询结果
// mysql_store_result()将查询的全部结果读取到客户端,分配1个MYSQL_RES结构,并将结果置于该结构中。
this->m_result = mysql_store_result(this->m_conn);
return false;
}
// 遍历查询得到的结果集
bool MysqlConn::next()
{
if (this->m_result!=nullptr)
{
// 会返回MYSQL_ROW类型的地址,是一个二级指针,指向一个指针数组,指针数组中的元素指向值
this->m_row = mysql_fetch_row(this->m_result);
}
return false;
}
// 得到结果集中的字段值
string MysqlConn::value(int index)
{
// 得到查询结果的列数
int columnCount = mysql_num_fields(this->m_result);
// 如果查询的索引越界
if (index >= columnCount || index < 0)
{
return string();
}
// 获取想要查询的值
char* val = m_row[index];
/*return string(val);*/ // 但是查询的结果中可能存在'\0'这样的字符,此时string()就会出问题
// 所有需要得到当前值的长度
unsigned long len = mysql_fetch_lengths(this->m_result)[index];
return string(val, len);
}
// 事务操作
bool MysqlConn::transaction()
{
// 第二个参数设置为false,为手动提交
return mysql_autocommit(this->m_conn,false);
}
// 提交事务
bool MysqlConn::commit()
{
return mysql_commit(this->m_conn);
}
// 事务回滚
bool MysqlConn::rollback()
{
return mysql_rollback(m_conn);
}
// 刷新起始的空闲时间点
void MysqlConn::refreshAliveTime()
{
// 得到当前的时间戳
this->m_alivetime = steady_clock::now();
}
// 计算连接存活的时长
long long MysqlConn::getAliveTime()
{
nanoseconds res = steady_clock::now() - this->m_alivetime;
milliseconds millisec = duration_cast<milliseconds>(res); // 类型转换,将低精度往高精度转换
return millisec.count(); // 计算millisec中有几个毫秒
}
// 释放资源
void MysqlConn::freeResource()
{
if (this->m_result)
{
mysql_free_result(this->m_result);
this->m_result = nullptr;
}
}
3.3 连接池代码设计
3.3.1 连接池connectionPool.h
cpp
#pragma once
#include<memory>
#include<queue>
#include<mutex>
#include<chrono>
#include"MysqlConn.h"
using namespace std;
using namespace chrono;
/*
* 数据库连接池: 单例模式
* MySqlConn 是一个连接MySQL数据库的类
*/
class connectionPool {
public:
// 通过静态方法得到单例对象
static connectionPool* getConnectPool();
// 从连接池中取出一个连接
shared_ptr<MysqlConn> getConnection();
// 析构函数
~connectionPool();
// 删除拷贝构造和拷贝赋值运算符重载函数,防止类的复制,实现单例模式
connectionPool(const connectionPool& obj) = delete;
connectionPool& operator=(const connectionPool& obj) = delete;
private:
// 构造函数私有化,防止外界可以实例化对象,打破单例模式
connectionPool();
// 解析json文件的函数
bool parseJsonFile();
// 生产数据库连接的函数
void produceConnection();
// 销毁数据库连接的函数
void recycleConnection();
// 创建新的连接对象
void addConnection();
string m_ip; // 数据库服务器ip地址
string m_user; // 数据库服务器用户名
string m_dbName; // 数据库服务器的数据库名
string m_passwd; // 数据库服务器密码
unsigned short m_port; // 数据库服务器绑定的端口
int m_minSize; // 连接池维护的最小连接数
int m_maxSize; // 连接池维护的最大连接数
int m_maxIdleTime; // 连接池中连接的最大空闲时长
int m_timeout; // 连接池获取连接的超时时长
queue<MysqlConn*> m_connectionQ; // 连接队列,连接数据库时可以在此进行连接
mutex m_mutexQ; // 互斥锁
condition_variable m_cond; // 条件变量
};
3.3.2 连接池connectionPool.cpp
cpp
#include "connectionPool.h"
#include<fstream>
#include<json/json.h>
#include<thread>
#include<iostream>
using namespace Json;
// 通过静态方法得到单例对象,饿汉模式
connectionPool* connectionPool::getConnectPool()
{
static connectionPool pool;
return &pool;
}
// 进行连接
shared_ptr<MysqlConn> connectionPool::getConnection()
{
unique_lock<mutex> locker(this->m_mutexQ);
// 判断连接队列是否为空
while (this->m_connectionQ.empty())
{
// wait_for() 当时间片转完还是没有被唤醒,则会返回timeout状态
// if判断语句为真,则说明连接队列为空
if (cv_status::timeout == this->m_cond.wait_for(locker, chrono::milliseconds(this->m_timeout)))// 为空就阻塞进程一定时间
{
if (this->m_connectionQ.empty()) // 保险起见又做一次判断
{
continue;
}
}
}
// 利用智能指针取出连接对象,智能指针可以直接指定删除器进行连接对象回收
shared_ptr<MysqlConn> connptr(this->m_connectionQ.front(), [this](MysqlConn* conn) { // 指定匿名删除器
// lock_guard在加锁后,可以自动在生命周期结束时对互斥锁进行解锁,一般是在函数结束时
lock_guard<mutex> locker(this->m_mutexQ);
// 重新指定被放回队列的时间
conn->refreshAliveTime();
// 放回连接队列
this->m_connectionQ.push(conn);
});
// 弹出被取出的连接对象
this->m_connectionQ.pop();
// 唤醒生产者线程
this->m_cond.notify_all();
return connptr;
}
connectionPool::~connectionPool()
{
while (!this->m_connectionQ.empty())
{
MysqlConn* conn = this->m_connectionQ.front();
this->m_connectionQ.pop();
delete conn;
conn = nullptr;
}
}
// 解析json文件的函数
bool connectionPool::parseJsonFile()
{
ifstream ifs("dbconn.config.json");
Reader rd;
Value root;
rd.parse(ifs, root); // 将ifs流对象中的json数据解析到root中
// 判断转换的是否是json数据,成功转化后,将其存储到成员变量中,并返回true
if (root.isObject())
{
this->m_ip = root["ip"].asString();
this->m_port = root["port"].asInt();
this->m_user = root["userName"].asString();
this->m_passwd = root["password"].asString();
this->m_dbName = root["dbName"].asString();
this->m_minSize = root["minsize"].asInt();
this->m_maxSize = root["maxsize"].asInt();
this->m_maxIdleTime = root["maxsize"].asInt();
this->m_timeout = root["timeout"].asInt();
return true;
}
return false;
}
// 当连接队列中连接对象太少时,需要创建一部分连接对象
void connectionPool::produceConnection()
{
while (true)
{
// unique_lock是个类模板
//mutex是参数的类型,unique_lock包装了一个互斥锁对象,当函数结束时,此unique_lock析构,进行解锁
unique_lock<mutex> locker(this->m_mutexQ);
while (this->m_connectionQ.size() >= this->m_minSize) // 当连接池中的连接数量大于最小数量时,就阻塞不需要生产连接
{
// 阻塞进程,不生产连接
m_cond.wait(locker);
}
// 生产连接
this->addConnection();
// 唤醒因没有连接对象而阻塞的线程
this->m_cond.notify_all();
}
}
// 当连接队列中连接对象太多时,需要销毁一部分连接对象释放资源
void connectionPool::recycleConnection()
{
while (true)
{
// 设置为每隔1检测一次
this_thread::sleep_for(chrono::seconds(1));
lock_guard<mutex> locker(this->m_mutexQ);
// 当连接队列元素个数大于最小连接数时
while (this->m_connectionQ.size() >= this->m_minSize)
{
// 检测连接对象的空闲时长,这个是从未使用开始计算的(即从被连接池中返还的连接队列时)
MysqlConn* conn = m_connectionQ.front();
if (conn->getAliveTime() >= this->m_maxIdleTime) // 检测到队头连接对象存活时间大于了最大空闲时间
{
this->m_connectionQ.pop();
delete conn;
}
else
{
break;
}
}
}
}
// 创建新的连接对象,并添加到连接队列中
void connectionPool::addConnection()
{
MysqlConn* conn = new MysqlConn;
conn->connect(m_user, m_passwd, m_dbName, m_ip, m_port);
conn->refreshAliveTime();
this->m_connectionQ.push(conn);
}
// 实现构造函数
connectionPool::connectionPool()
{
// 加载配置文件
if (!this->parseJsonFile())
{
// 未加载成功
return;
}
int num = 0;
// 初始化最小连接数的连接对象
for (int i = 0; i < this->m_minSize; i++)
{
this->addConnection();
cout << ++num << endl;
}
// 将生产/销毁连接交给子线程去做
// 这里将子线程的任务函数设置为类的非静态成员函数
thread producer(&connectionPool::produceConnection,this);
thread recycler(&connectionPool::recycleConnection,this);
// 将主线程和子线程分离,避免阻塞主线程
producer.detach();
recycler.detach();
}
3.4 测试函数
cpp
#include<iostream>
#include<string>
#include"MysqlConn.h"
#include"connectionPool.h"
using namespace std;
// @file:connectionPool
// @author:IdealSanX_T
// @date:2024/7/3 9:56:19
// @brief:连接线程池测试函数
// 单线程不使用线程池
void op1(int begin, int end)
{
for (int i = begin; i < end; ++i)
{
MysqlConn conn;
conn.connect("root", "123456", "504", "127.0.0.1");
char sql[1024] = { 0 };
sprintf(sql, "insert into test values (%d,25,' man', 'tom')",i);
conn.update(sql);
}
}
// 单线程使用线程池
void op2(connectionPool* pool,int begin, int end)
{
for (int i = begin; i < end; ++i)
{
shared_ptr<MysqlConn> conn = pool->getConnection();
char sql[1024] = { 0 };
sprintf(sql, "insert into test values (%d,25,' man', 'tom')", i);
conn->update(sql);
cout << "插入数据" << endl;
}
}
// 单线程测试函数
void test01()
{
#if 0
// 非连接池,单线程,用时:4592767400纳秒,4592毫秒
steady_clock::time_point begin = steady_clock::now();
op1(0, 5000);
steady_clock::time_point end = steady_clock:: now();
auto length = end - begin;
cout << "非连接池,单线程,用时:" << length.count() << "纳秒,"
<< length.count() / 1000000 << "毫秒" << endl;
#else
// 连接池,单线程,用时:2199883200纳秒,2199毫秒
connectionPool* pool = connectionPool::getConnectPool();
steady_clock::time_point begin = steady_clock::now();
op2(pool, 0, 5000);
steady_clock::time_point end = steady_clock::now();
auto length = end - begin;
cout << "连接池,单线程,用时:" << length.count() << "纳秒,"
<< length.count() / 1000000 << "毫秒" << endl;
#endif
}
// 多线程测试函数
void test02()
{
#if 0
// 非连接池,多单线程,用时:2791891000纳秒,2791毫秒
MysqlConn conn;
conn.connect("root", "123456", "504", "127.0.0.1");
steady_clock::time_point begin = steady_clock::now();
thread t1(op1, 0, 1000);
thread t2(op1, 1000, 2000);
thread t3(op1, 2000, 3000);
thread t4(op1, 3000, 4000);
thread t5(op1, 4000, 5000);
t1.join();
t2.join();
t3.join();
t4.join();
t5.join();
steady_clock::time_point end = steady_clock::now();
auto length = end - begin;
cout << "非连接池,多单线程,用时:" << length.count()<<"纳秒,"
<< length.count() / 1000000 << "毫秒" <<endl;
#else
// 连接池,多单线程,用时:1974331000纳秒,1974毫秒
connectionPool* pool = connectionPool::getConnectPool();
steady_clock::time_point begin = steady_clock::now();
thread t1(op2, pool, 0, 1000);
thread t2(op2, pool, 1000, 2000);
thread t3(op2, pool, 2000, 3000);
thread t4(op2, pool, 3000, 4000);
thread t5(op2, pool, 4000, 5000);
t1.join();
t2.join();
t3.join();
t4.join();
t5.join();
steady_clock::time_point end = steady_clock::now();
auto length = end - begin;
cout << "连接池,多单线程,用时:" << length.count() <<"纳秒,"
<< length.count() / 1000000 << "毫秒" << endl;
#endif
}
int main() {
//test01();
test02();
system("pause");
return 0;
}
四、 总结
使用连接池时,比不使用连接池所用时更短,效果更好。
可以在消费者/生产者线程中进行代码优化。
问题总结
MySQL "error: 'fd': 未知重写说明符"
增加引用头文件
当用C/C++ 连接数据库并且采用ODBC(Open DataBases Connection) 肯定会出现
#include 这个头文件,关键就是这个头文件的问题,
在预编译 #include <mysql.h> 一定要先包含 #include <winsock.h> 这个头文件才不会出现刚才的问题。
winsock.h这个头文件一定要在mysql.h的头文件前面。
原因:#include "mysql.h"中调用了mysql_com.h,而mysql_com.h使用了有关网络套接字的fd,所以如果没有网络通信的头文件的话,就会报错。
cpp
#include <WinSock2.h>//必须要有,解决"fd":未知重写说明符
LNK2019 无法解析的外部符号 _mysql_set_character_set@8,
原因如下,系统是win10x64,MySQL 64位的lib也是64位的接口。所以解决方法如下:
.项目->属性->配置管理器
活动解决方案平台,下拉选新建,出现一个新的对号框,在键入选择新平台中选择X64
然后把VS配置的include目录和lib目录等再次配置一下。
重新编译 成功~~~
由于找不到libmysql.dIl, 无法继续执行代码。重新安装程序可能会解决此问题
解决方法:
1.首先找到 libmysql.dll 文件。(在MySQL解压后的文件夹中的lib文件夹目录下)
2.然后直接将"libmysql.dll"复制到项目同级目录下,问题解决。
( 我的项目名为SQLConnectionPool,我就把文件复制到SQLConnectionPool目录下,如图所示)