池化技術原理,手寫數據庫連接池

池化技術在後端開發中應用非常廣泛,有數據庫連接池,線程池,對象池,常量池等。池化技術的出現是爲了提高性能。實際就是對一些使用率較高,且創建銷燬比較耗時的資源進行緩存,避免重複地創建和銷燬,做到資源回收利用。

傳統的數據庫連接池有DBCP,C3P0,目前新一代的連接池Druid、HikariaCP等較傳統的數據庫連接池在性能上有很大的提升。本文將手寫一個簡單功能的數據庫連接池,以理解數據庫連接池的基本原理和使用。

一、原生數據庫訪問方式所帶來的問題

一般來說,java應用訪問數據庫有以下幾步操作:

  • 裝載數據庫驅動,

    Class.forName(JDBC_DRIVER);

  • 通過jdbc建立數據庫連接,

    DriverManager.getConnection(JDBC_URL, JDBC_USERNAME, JDBC_PASSWORD)

  • 訪問數據庫,執行sql語句

    String sql = "select email from user where id = 5";
    PreparedStatement statement = conn.prepareStatement(sql);
    ResultSet result = statement.executeQuery();
    while (result.next()){
        String email = result.getString(1);
        System.out.println("【email】:"+email);
    }
    
  • 斷開數據庫連接

    result.close(); state.close(); conn.close();

每一個請求過來都要建立數據庫連接,連接數據庫需要建立網絡連接、系統要分配內存資源、使用完必須斷開連接,這些過程可能比實際進行的數據庫操作耗時好多倍。並且在高併發量的請求情況下,應用響應速度就會非常慢,甚至造成服務器崩潰。

由此可見,數據庫連接是一種非常昂貴的資源。

二、數據庫連接池技術

爲解決上述問題,可以採用數據庫連接池技術。數據庫連接池的基本思想就是爲數據庫連接建立一個“緩衝池”。初始化時在緩衝池中放入指定數量的連接,當需要建立數據庫連接時,只需從“緩衝池”中取出一個,使用完畢之後再放回去。我們可以通過設定各種參數來控制初始連接數量、最大連接數量、最大空閒連接數量、等待時間等,還可以通過連接池的管理機制來監視數據庫連接的數量和使用情況,方便開發測試和性能調優。

有了這些需求下面來實現一個簡單的數據庫連接池,實現連接池初始化方法、獲取連接方法、回收連接方法、銷燬池方法,以及幾個參數的可配置。

1、工具類:MysqlConnectionUtils

首先將jdbc層面的代碼封裝成工具類,提供獲取連接,關閉連接的方法,以Mysql爲例,這部分代碼不是重點,所以配置信息也直接寫到類裏面了:

public class MysqlConnectionUtils {
    public static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";
    public static final String JDBC_URL = "jdbc:mysql://localhost:3306/usercase?useUnicode=true&characterEncoding=utf-8";
    public static final String JDBC_USERNAME = "root";
    public static final String JDBC_PASSWORD = "123456";
    public static Connection getConnection() {
        Connection connection=null;
        try {
            Class.forName(JDBC_DRIVER);
            connection = DriverManager.getConnection(JDBC_URL, JDBC_USERNAME, JDBC_PASSWORD);
        } catch (ClassNotFoundException | SQLException e) {
            e.printStackTrace();
        }
        return connection;
    }
    public static void closeConnection(Connection conn){
        if (conn == null) {return;}
        try {
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
2、定義接口:ConnectionPool

定義接口:

public interface ConnectionPool {
    /*初始化*/
    void init(int initSize,int maxSize,int idleCount,long waitTime);
    /*獲取連接*/
    Connection getConnection() throws TimeoutException;
    /*回收*/
    void recycle(Connection conn);
    /*銷燬*/
    void destory();
}
3、實現類:MyConnectionPool

屬性

這個就是我們要實現的線程池了,首先看該類的一些屬性:

public class MyConnectionPool implements ConnectionPool {

    //初始化連接數量
    int initSize;
    //最大連接數量
    int maxSize;
    //最大空閒連接數量
    int idleCount;
    //最大等待時間
    long waitTime;
    //活躍連接數
    AtomicInteger activeSize;
    //空閒隊列
    BlockingQueue<Connection> idle;
    //已使用隊列
    BlockingQueue<Connection> busy;

這裏定義了兩個隊列,一個存放空閒連接,一個存放正在使用的連接。活躍連接數就是連接池中未關閉的連接的總數,也就是兩個隊列的連接總數。

取出連接和回收連接方法模型圖:

在這裏插入圖片描述

構造器

通過構造方法完成初始化:

	public MyConnectionPool(int initSize, int maxSize, int idleCount, long waitTime) {
        init(initSize ,maxSize,idleCount,waitTime);
    }

    public MyConnectionPool() {}

    @Override
    public void init(int initSize, int maxSize, int idleCount, long waitTime) {
        this.initSize = initSize;
        this.maxSize = maxSize;
        this.idleCount = idleCount;
        this.waitTime = waitTime;
        this.activeSize = new AtomicInteger();
        this.idle = new LinkedBlockingQueue<Connection>();
        this.busy = new LinkedBlockingQueue<Connection>();
        //初始化連接
        initConnection(initSize);
    }

    private void initConnection(int initSize) {
        for (int i = 0; i <initSize ; i++) {
            if (activeSize.get()<maxSize){//兩重判斷是爲了保證線程安全
                if (activeSize.incrementAndGet()<=maxSize){
                    //創建連接
                    Connection conn = MysqlConnectionUtils.getConnection();
                    //加入空閒隊列
                    idle.offer(conn);
                }else{
                    activeSize.decrementAndGet();
                }
            }
        }
    }

構造線程池時,輸入四個初始化參數,分別是初始化連接數、最大連接數、最大空閒連接數和超時時間。在構造方法中初始化兩個核心隊列,使用LinkedBlockingQueue有序阻塞隊列。並在構造方法中創建初始化連接數數量的連接,放入空閒隊列,注意每成功創建一個連接,活躍連接數activeSize加1

這裏activeSize使用原子類AtomicInteger,是考慮多線程的情況下線程安全問題,防止創建的線程數超出。

初始化方法兩重if判斷中進行原子操作,可形成鎖,保證線程安全。

接下來實現從連接池中獲取連接的方法:

獲取連接方法:getConnection()
    @Override
    public Connection getConnection() throws TimeoutException {
        long startTime = System.currentTimeMillis();
        //1.從空閒隊列裏獲取,return
        Connection conn = idle.poll();
        if (conn != null) {
            busy.offer(conn);
            System.out.println("["+Thread.currentThread().getName()+"]"+" ************從空閒隊列獲取連接************");
            return conn;
        }
        //2.空閒隊列沒有連接了,並且活躍連接數量不超過最大連接數量,創建一個新的連接return
        if (activeSize.get()<maxSize){//兩重判斷是爲了保證線程安全
            if (activeSize.incrementAndGet()<=maxSize){
                conn = MysqlConnectionUtils.getConnection();
                System.out.println("["+Thread.currentThread().getName()+"]"+" ************創建一個新的連接************");
                busy.offer(conn);
                return conn;
            }else {
                activeSize.decrementAndGet();
            }
        }
        //3.連接全部正在使用,並且活躍連接數超過最大連接數量,則等待正在使用的連接被回收到空閒隊列。
        //waitTime代表getConnection()方法的等待時間,poll方法阻塞時間其中一部分
        long timeout = waitTime - (System.currentTimeMillis()-startTime);
        try {
            //阻塞方法,等待從idle隊列獲取連接
            conn = idle.poll(timeout, TimeUnit.MILLISECONDS);
            if (conn == null) {
                throw new TimeoutException("["+Thread.currentThread().getName()+"]"+" ************獲取連接超時,等待時間爲"+timeout+"ms************");
            }
            busy.offer(conn);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return conn;
    }

以上代碼核心就是兩個隊列彈出元素和加入元素的過程。然後插入了一些打印信息,方便測試的時候查看。

獲取連接實際上有三種可能的方式以及一種超時拋異常的情況:

  • 空閒隊列中有可用連接,直接從空閒隊列彈出
  • 空閒隊列沒有連接了,並且活躍連接數沒有超過配置的最大連接數,此時自己創建連接給到調用者,注意每成功創建一個連接,活躍連接數activeSize加1。(從這裏可以看出,如果最大空閒連接數設置得過小,最大連接數遠大於最大空閒連接數的話,線程池會大量創建連接,性能就會比較差。)
  • 空閒隊列沒有連接,並且活躍連接數達到配置的最大連接數,此時不能在創建連接,只能阻塞等待,當超時時間內有連接被回收進空閒隊列,阻塞結束,獲得連接。
  • 當達到超時時間沒有連接被回收,拋出超時異常。(這裏超時時間的配置應當根據多數線程佔用連接的時間決定

然後看另一個關鍵的方法,回收方法:

回收方法:recycle()
    @Override
    public void recycle(Connection conn) {
        if (conn == null) { return;}
        //回收連接,就是將連接從使用隊列移動到空閒隊列
        if (busy.remove(conn)){//第一步,移除
            //如果實際的空閒連接數量大於用戶指定的最大空閒連接數量,則連接多餘了,關閉連接
            if (idle.size()>=idleCount){
                MysqlConnectionUtils.closeConnection(conn);
                //關閉連接,則活躍連接減一
                activeSize.decrementAndGet();
                System.out.println("["+Thread.currentThread().getName()+"]"+" ++++空閒連接數量太多,關閉連接++++");
                return;
            }
            if (!idle.offer(conn)){//第二步,放入,如果放入空閒隊列失敗,關閉連接
                MysqlConnectionUtils.closeConnection(conn);
                activeSize.decrementAndGet();
            }
            System.out.println("["+Thread.currentThread().getName()+"]"+" ++++回收連接成功++++");
        }else{//從busy隊列移除失敗,可能因爲該連接不在busy隊列中
            MysqlConnectionUtils.closeConnection(conn);
            activeSize.decrementAndGet();
            System.out.println("["+Thread.currentThread().getName()+"]"+" ++++未知連接,直接關閉,不予回收++++");
        }
    }

回收方法實際就是兩步,

  • 將指定的連接從busy隊列中彈出,如果該連接不是busy隊列中的連接,也就是彈出失敗,就直接關閉它。
  • 檢查空閒隊列內的連接數,如果達到配置的最大空閒連接數,則直接關閉連接。如果沒有達到,就將該連接放入空閒隊列。

注意每關閉一個連接,活躍連接數activeSize減1。

連接池銷燬方法:destroy()
	@Override
    public void destory() {
        while (busy.size()!=0){
            recycle(busy.poll());
        }
        while(idle.size()!=0){
            MysqlConnectionUtils.closeConnection(idle.poll());
            activeSize.decrementAndGet();
        }
    }

將連接全部回收,然後關閉所有控線連接。

至此,一個簡單的數據庫連接池實現完畢。核心方法就是獲取連接方法和回收方法。下面通過設置不同的參數來測試一下功能好不好使,以及分析這些參數應當根據那些因素來配置。

三、測試連接池

測試類代碼如下:

public class TestMain {
    private static int threadNum = 50;

    public static void main(String[] args) {
        String sql = "select email from user where id = 5";
        CountDownLatch cdl = new CountDownLatch(1);
        ConnectionPool pool = new MyConnectionPool(10,20,15,20000L);
        for (int i = 0; i <threadNum ; i++) {
            new Thread(() ->{
                Connection conn = null;
                try {
                    cdl.await();  //阻塞在這,等待所有線程準備好,發令槍響,一起執行
                    conn = pool.getConnection();
                    PreparedStatement statement = conn.prepareStatement(sql);
                    ResultSet result = statement.executeQuery();
                    while (result.next()){
                        String email = result.getString(1);
                        System.out.println("【email】:"+email);
                    }
                    //Thread.currentThread().sleep(1000L*new Random().nextInt(5));//模擬線程佔用連接的時間
                } catch (TimeoutException | InterruptedException | SQLException e) {
                    e.printStackTrace();
                } finally {
                    pool.recycle(conn);
                }
            }).start();

        }
        cdl.countDown();  //發令槍,喚醒所有線程
        sleep(3000);//等待所有線程執行完畢
        System.out.println("已建立連接數:"+((MyConnectionPool) pool).activeSize.get()+
                ",空閒連接數:"+((MyConnectionPool) pool).idle.size()+
                ",正在使用連接數:"+((MyConnectionPool) pool).busy.size());
        pool.destory();
        System.out.println("detroy MyConnectionPool!!");
        System.out.println("已建立連接數:"+((MyConnectionPool) pool).activeSize.get()+
                ",空閒連接數:"+((MyConnectionPool) pool).idle.size()+
                ",正在使用連接數:"+((MyConnectionPool) pool).busy.size());
    }

    public static void sleep(long time){
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

首先設置50個線程併發訪問連接池,初始化連接數10、最大連接數20、最大空閒連接數15、超時時間20秒,

ConnectionPool pool = new MyConnectionPool(10,20,15,20000L);

測試結果:

[Thread-1] 從空閒隊列獲取連接

[Thread-17] 創建一個新的連接

【email】:[email protected]

[Thread-20] ++++回收連接成功++++

[Thread-23] ++++空閒連接數量太多,關閉連接++++

已建立連接數:15,空閒連接數:15,正在使用連接數:0

detroy MyConnectionPool!!
已建立連接數:0,空閒連接數:0,正在使用連接數:0

由於篇幅原因,打印信息省略一部分。

本次測試得到以下信息:

  • 由於設置的超時時間是20秒,遠大於各線程佔用時間,所以沒有出現超時異常的情況。
  • 初始化了10個連接,“創建一個連接”輸出了10行,“空閒連接數量太多,關閉連接”輸出了5行,所有線程執行完畢後,已建立連接數:15,空閒連接數:15,正在使用連接數:0。說明沒有連接泄露。
  • 最大空閒連接數實際就是併發量夠大時的連接的緩存容量,

更改參數再次測試,設置超時時間爲3秒,模擬線程隨機1到5秒的佔用連接時間,主線程等待10秒:

ConnectionPool pool = new MyConnectionPool(10,20,15,3000L);

Thread.currentThread().sleep(1000L*new Random().nextInt(5));//模擬線程佔用連接的時間

sleep(10000);

[Thread-1] 從空閒隊列獲取連接

[Thread-17] 創建一個新的連接

【email】:[email protected]

java.util.concurrent.TimeoutException: [Thread-38] 獲取連接超時,等待時間爲3000ms
at com.youzi.test.concurrent.connPool.MyConnectionPool.getConnection(MyConnectionPool.java:100)
at com.youzi.test.concurrent.connPool.TestMain.lambda$main$0(TestMain.java:23)
at java.lang.Thread.run(Thread.java:745)

[Thread-20] ++++回收連接成功++++

[Thread-23] ++++空閒連接數量太多,關閉連接++++

已建立連接數:15,空閒連接數:15,正在使用連接數:0

detroy MyConnectionPool!!
已建立連接數:0,空閒連接數:0,正在使用連接數:0

可以看到由於線程佔用連接時間爲隨機1到5秒,而設置超時時間是3秒,在被佔用連接數量達到最大連接數20時,某線程3秒內沒有獲取到連接,就會拋出超時異常。不影響其他線程。

線程池連接數量始終符合根據配置該有的數量。沒有超出配置,也沒有連接泄露。


手寫連接池測試完畢。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章