歡迎訪問的另一個部落格: https://xingzhu.top/
原始碼連結: https://github.com/xingzhuz/MysqlLinkPool
前置知識:
相關的環境配置: https://xingzhu.top/archives/shu-ju-ku-lian-jie-chi-huan-jing-pei-zhi
MySQL API: https://subingwen.cn/mysql/mysql-api/
Jsoncpp API: https://xingzhu.top/archives/jsoncpp
C++多執行緒: https://xingzhu.top/archives/duo-xian-cheng-xian-cheng-chi
概述
- 資料庫連線池的作用是管理和複用資料庫連線,以提高應用程式的效能和資源利用率
- 透過預先建立一定數量的連線並將它們儲存在池中,應用程式可以快速獲取連線,而無需每次都進行昂貴的連線建立和銷燬過程
- 這不僅減少了延遲,還能有效控制資料庫連線的數量,防止資源耗盡,從而提升整體系統的穩定性和響應速度
具體實現
- 連線池只需要一個例項,所以連線池類應該是一個單例模式的類
ConnectionPool *ConnectionPool::getConnectPool()
{
// 建立靜態佈局變數,訪問範圍就只有這個函式作用域,但是生命週期是到程式結束
// 第一次呼叫,會在建立記憶體地址,但是第二次就直接使用這個地址了,不會再次建立,就返回之前建立的例項了
// 這裡使用的是單例模式中的懶漢模式,呼叫時建立
static ConnectionPool pool;
return &pool;
}
- 所有的資料庫連線應該維護到一個佇列中,使用佇列的目的是方便連線的新增和刪除
- 由於佇列是連線池共享資源,需要使用互斥鎖來保護佇列資料的讀寫
- 由於資料庫有連線上限,過多會壓力過大,導致效能降低,所以需要設定一個最大連線上限;為了應對突然的高併發操作,需要設定一個最小連線數,這個最小連線數是維護佇列的最小數量,保證有這麼多連線供使用
- 客戶端在滿足條件的情況下,從連線池取連線,然後從進行使用,使用完畢後,歸還這個連線,讓這個連線重新加入佇列中,不是銷燬掉
- 因此佇列儲存的是一個資料庫連線的物件,即
MYSQL*
,為了便於使用,先封裝一個MYSQL
類,用於連線
封裝資料庫標頭檔案
#pragma once
#include <iostream>
#include <mysql.h>
#include <chrono>
using namespace std;
using namespace chrono;
class MysqlConn
{
public:
// 初始化資料庫連線
MysqlConn();
// 釋放資料庫連線
~MysqlConn();
// 連線資料庫
bool connect(string user, string passwd, string dbName, string ip, unsigned short port = 3306);
// 更新資料庫: insert, update, delete
bool update(string sql);
// 查詢資料庫 select 語句
bool query(string sql);
// 遍歷查詢得到的結果集
bool next();
// 得到結果集中的欄位值
string value(int index);
// 事務操作
bool transaction();
// 提交事務
bool commit();
// 事務回滾
bool rollback();
// 重新整理起始的空閒時間點
void refreshAliveTime();
// 計算連線存活的總時長
long long getAliveTime();
private:
void freeResult(); // 釋放結果集記憶體
MYSQL *m_conn = nullptr; // 資料庫物件
MYSQL_RES *m_result = nullptr; // select 查詢後的結果集
MYSQL_ROW m_row = nullptr; // 用於遍歷結果集,儲存一行資料
steady_clock::time_point m_alivetime; // 建立絕對時鐘
};
連線池標頭檔案
#pragma once
#include <queue>
#include <mutex>
#include <condition_variable>
#include <thread>
#include "MysqlConn.h"
#include <atomic>
using namespace std;
class ConnectionPool
{
public:
// 設定靜態方法,因為不能建立物件,所以設定為靜態方法,透過類名獲取這個資料庫連線池例項
static ConnectionPool *getConnectPool();
// 刪除複製建構函式和 "=" 運算子過載,因為要保證只有一個連線池例項
ConnectionPool(const ConnectionPool &obj) = delete;
ConnectionPool &operator=(const ConnectionPool &obj) = delete;
// 外部呼叫這個,取出一個資料庫連線
shared_ptr<MysqlConn> getConnection();
~ConnectionPool();
private:
// 建構函式私有化,不允許外部建立,因為保持一個物件,單例模式
ConnectionPool();
// 解析 json資料
bool parseJsonFile();
// 建立新的連線的執行緒處理動作函式
void produceConnection();
// 銷燬連線執行緒的執行緒處理動作函式
void recycleConnection();
// 增加連線
void addConnection();
string m_ip; // 資料發伺服器的 IP地址
string m_user; // 資料庫伺服器使用者名稱
string m_passwd; // 對應的密碼
string m_dbName; // 資料庫伺服器上對應的資料庫名
unsigned short m_port; // 資料庫伺服器埠號
int m_minSize; // 連線數量的最小值,這個是維護佇列的最小值,保證突然的大量連線請求
int m_maxSize; // 連線數量的最大值,這個是佇列數量 + m_busySize,表示最多隻支援的連線數,包括了正在使用的
atomic<int> m_busySize; // 正在忙的連線數,這個是共享資源,設定為原子變數
int m_timeout; // 超時時長,當連線數用完後等待的阻塞時長
int m_maxIdleTime; // 最大空閒時長,決定是否斷開這個資料庫連線
// 連線池佇列,儲存的時是若干個資料庫連線
queue<MysqlConn *> m_connectionQ;
mutex m_mutexQ; // 連線池佇列的互斥鎖
condition_variable m_cond; // 條件變數
};
建構函式
- 首先解析 json 格式資料,設定埠號和 IP
- 然後以最小連線數新增連線到佇列
- 建立兩個執行緒進行管理新增連線和銷燬連線的實現
新增連線
- 這個使用一個執行緒單獨維護,一直執行,除非連線池傳送一個關閉訊號
- 持續判斷能否新增連線
// 建立新的連線
void ConnectionPool::produceConnection()
{
while (true)
{
if (isShutdown)
return;
// 最小連線數,保證佇列裡有最小連線數,即使不使用,應對突然的高數量訪問
unique_lock<mutex> locker(m_mutexQ);
while (m_connectionQ.size() >= m_minSize || m_connectionQ.size() + m_busySize >= m_maxSize)
{
m_cond.wait(locker);
// 這個非常重要,如果是因為連線池關閉喚醒,直接退出
if (isShutdown)
return;
}
addConnection(); // 將連線加入佇列 (封裝的函式)
m_cond.notify_all(); // 喚醒阻塞在佇列為空的執行緒
}
}
- 如果佇列的數量比最小連線數大,就不需要新增連線
- 如果佇列的連線數量 + 忙的連線 (正在被使用的) 超過了連線上限,就不需要往佇列裡生產連線了,即使此時佇列的數量比最小連線數小,仍不能新增連線,因為新增連線後,客戶端就能取,如果都取了,就超過連線上限了,因此維護的是佇列連線數 + 忙的連線數要小於最大連線上限
- 也就是隻有佇列的數量比最小連線數小,並且此時佇列的連線數量 + 忙的連線 (正在被使用的) 小於連線上限,才新增連線到佇列
銷燬連線
- 也是使用一個執行緒隔一段時間就進行檢測,銷燬的連線是佇列中的空閒連線
- 因此取頭部連線,計算它的存活時間,如果大於我們設定的值,就銷燬這個連線,彈出佇列,銷燬它是因為它一直沒被使用,屬於空閒連線,只需一直銷燬到保持有最小連線數即可
- 注意銷燬彈出之前,需要滿足佇列的數量大於最小連線數,如果小於,就不進行這個操作
// 銷燬連線
// 銷燬的空閒連線,滿足超過一定時長還是空閒就銷燬
void ConnectionPool::recycleConnection()
{
while (true)
{
if (isShutdown)
return;
this_thread::sleep_for(chrono::milliseconds(500));
unique_lock<mutex> locker(m_mutexQ);
while (m_connectionQ.size() > m_minSize)
{
MysqlConn *conn = m_connectionQ.front();
// 存活的時間
if (conn->getAliveTime() >= m_maxIdleTime)
{
m_connectionQ.pop();
delete conn;
}
else
break;
}
}
}
取連線
- 首先需要檢測這個佇列是否為空,和現在忙的 (即正在使用的連線) 數量是否大於了連線上限,如果滿足這二者之中的任何一個,都對其進行阻塞等待,不取連線
- 這個阻塞設定一個超時時間,如果滿足上述二者只中一個,就阻塞,達到阻塞時長後,這個阻塞會解除,自己進行選擇是退出還是繼續進行阻塞
- 取連線後,由於這個連線不使用需要回收,重新加入佇列中,因此需要儲存這個連線,這個是一個指標,因此考慮共享智慧指標維護,更安全
- 並且這個共享智慧指標還有個功能,可以指定刪除器函式,也就是這個指標生命週期結束後的指標處理,預設是清除智慧指標指向的地址,由於這裡不是進行銷燬,我們需要重新加入佇列,因此重定義刪除器函式,使之為這個指標 (這個資料庫連線)重新加入佇列
// 外部呼叫這個,取出一個資料庫連線
shared_ptr<MysqlConn> ConnectionPool::getConnection()
{
// 這個能判斷四種情況
// 1. 如果沒達到最大連線數請求,但是佇列為空,阻塞
// 2. 如果沒達到最大連線數請求,但是佇列不為空,不阻塞
// 3. 如果達到最大連線數請求,但是佇列不為空,阻塞
// 4. 如果達到最大連線s數請求,但是佇列為空,阻塞
// 只要沒達到最大連線數以及佇列不為空,才能取連線,否則阻塞
// 設定的超時檢測,超過時長沒被喚醒,則繼續執行 while 判斷是否繼續阻塞
unique_lock<mutex> locker(m_mutexQ);
while (m_connectionQ.empty() || (m_busySize >= m_maxSize))
{
// 阻塞指定的時間長度,時間到了就解除阻塞
if (cv_status::timeout == m_cond.wait_for(locker, chrono::milliseconds(m_timeout)))
{
// 進入這個 if 說明阻塞 m_timeout 這個時長,還是沒有被喚醒
if (m_connectionQ.empty() || (m_busySize >= m_maxSize))
{
// 兩種方式,continue繼續阻塞,或者 return退出
// return nullptr;
continue;
}
}
}
// 使用共享的智慧指標,回收當前連線用完後,重新 Push進佇列中,自動回收
// 由於這個共享智慧指標預設刪除器處理動作是:回收這個智慧指標指向的記憶體地址
// 但是我們不是要回收,我們是要重新加入佇列,因此重新指定刪除器函式
// 也就是第二個引數,這裡使用 lambda方式實現
shared_ptr<MysqlConn> connptr(
m_connectionQ.front(),
[this](MysqlConn *conn)
{
unique_lock<mutex> locker(m_mutexQ);
// 更新加入佇列的時間
conn->refreshAliveTime();
m_connectionQ.push(conn);
m_busySize--;
m_cond.notify_all(); // 喚醒因最大連線數阻塞的執行緒
});
m_connectionQ.pop();
m_busySize++;
// 本意是喚醒生產者執行緒,也就是喚醒建立新連線的執行緒
// 雖然會喚醒取連線的執行緒,但是不影響,它仍會繼續阻塞
m_cond.notify_all();
return connptr;
}
解析 Json
- 為了不寫死這個資料庫連線的 IP 和埠號,這裡使用 json 格式讀取進去賦值
// 解析 json資料
bool ConnectionPool::parseJsonFile()
{
ifstream ifs("dbconf.json");
Reader rd;
Value root;
rd.parse(ifs, root);
if (root.isObject())
{
m_ip = root["ip"].asString();
m_port = root["port"].asInt();
m_user = root["userName"].asString();
m_passwd = root["password"].asString();
m_dbName = root["dbName"].asString();
m_minSize = root["minSize"].asInt();
m_maxSize = root["maxSize"].asInt();
m_maxIdleTime = root["maxIdleTime"].asInt();
m_timeout = root["timeout"].asInt();
m_busySize = 0;
return true;
}
return false;
}
說明: 參考學習 https://www.bilibili.com/video/BV1Fr4y1s7w4/?spm_id_from=333.999.0.0