提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、连接池设计
- 1.1 功能设计
- 1.2 封装设计
- 二、连接数据库步骤
- 三、封装编码
- 3.1 VS配置
- 3.1.1 MySQL环境
- 3.1.2 jsoncpp环境
- 3.2 连接类代码设计
- 3.2.1 连接类MysqlConn.h
- 3.2.1 连接类MysqlConn.cpp
- 3.3 连接池代码设计
- 3.3.1 连接池connectionPool.h
- 3.3.2 连接池connectionPool.cpp
- 3.4 测试函数
- 四、 总结
- 问题总结
- MySQL “error: ‘fd’: 未知重写说明符”
- LNK2019 无法解析的外部符号 _mysql_set_character_set@8,
- 由于找不到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
- 连接池连接的动态创建:这部分工作需要交给一个单独的线程来处理
- 连接池连接的动态销毁:这部分工作需要交给一个单独的线程来处理
- 数据库连接的添加和归还:这是一个典型的生产者和消费者模型
- 消费者:需要访问数据库的线程,数据库连接被取出(消费)
- 生产者:专门负责创建数据库连接的线程
- 处理生产者和消费者模型需要使用条件变量阻塞线程
- 连接池的默认连接数量:连接池中提供的可用连接的最小数量
- 如果不够就动态创建
- 如果太多就动态销毁
- 连接池的最大连接数量:能够创建的最大有效数据库连接上限
- 最大空闲时间:创建出的数据库连接在指定时间长度内一直未被使用,此时就需要销毁该连接。
- 连接超时:消费者线程无法获取到可用连接是,阻塞等待的时间长度
综上所述,数据库连接池对应的单例模式的类的设计如下:
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
#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
#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,执行成功返回0if (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
#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
#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数据,成功转化后,将其存储到成员变量中,并返回trueif (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 测试函数
#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,所以如果没有网络通信的头文件的话,就会报错。
#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目录下,如图所示)