資料庫連線池實現

星竹z發表於2024-09-29

歡迎訪問的另一個部落格: 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

概述

  • 資料庫連線池的作用是管理和複用資料庫連線,以提高應用程式的效能和資源利用率
  • 透過預先建立一定數量的連線並將它們儲存在池中,應用程式可以快速獲取連線,而無需每次都進行昂貴的連線建立和銷燬過程
  • 這不僅減少了延遲,還能有效控制資料庫連線的數量,防止資源耗盡,從而提升整體系統的穩定性和響應速度

Pasted image 20240929162257

具體實現

  • 連線池只需要一個例項,所以連線池類應該是一個單例模式的類
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

相關文章