聊聊資料庫連線池 Druid

2023-12-09 12:00:46

在 Spring Boot 專案中,資料庫連線池已經成為標配,然而,我曾經遇到過不少連線池異常導致業務錯誤的事故。很多經驗豐富的工程師也可能不小心在這方面出現問題。

在這篇文章中,我們將探討資料庫連線池,深入解析其實現機制,以便更好地理解和規避潛在的風險。

1 為什麼需要連線池

假如沒有連線池,我們運算元據庫的流程如下:

  1. 應用程式使用資料庫驅動建立和資料庫的 TCP 連線 ;
  2. 使用者進行身份驗證 ;
  3. 身份驗證通過,應用進行讀寫資料庫操作 ;
  4. 操作結束後,關閉 TCP 連線 。

建立資料庫連線是一個比較昂貴的操作,若同時有幾百人甚至幾千人線上,頻繁地進行連線操作將佔用更多的系統資源,但資料庫支援的連線數是有限的,建立大量的連線可能會導致資料庫僵死。

當我們有了連線池,應用程式啟動時就預先建立多個資料庫連線物件,然後將連線物件儲存到連線池中。當客戶請求到來時,從池中取出一個連線物件為客戶服務。當請求完成時,客戶程式呼叫關閉方法,將連線物件放回池中。

相比之下,連線池的優點顯而易見:

1、資源重用:

因為資料庫連線可以重用,避免了頻繁建立,釋放連線引起的大量效能開銷,同時也增加了系統執行環境的平穩性。

2、提高效能

當業務請求時,因為資料庫連線在初始化時已經被建立,可以立即使用,而不需要等待連線的建立,減少了響應時間。

3、優化資源分配

對於多應用共用同一資料庫的系統而言,可在應用層通過資料庫連線池的設定,實現某一應用最大可用資料庫連線數的限制,避免某一應用獨佔所有的資料庫資源。

4、連線管理

資料庫連線池實現中,可根據預先的佔用超時設定,強制回收被佔用連線,從而避免了常規資料庫連線操作中可能出現的資源洩露。

2 JDBC 連線池

下面的程式碼展示了 JDBC 運算元據庫的流程 :

//1. 連線到資料庫
Connection connection = DriverManager.getConnection(jdbcUrl, username, password);
//2. 執行SQL查詢
String sqlQuery = "SELECT * FROM mytable WHERE column1 = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sqlQuery);
preparedStatement.setString(1, "somevalue");
resultSet = preparedStatement.executeQuery();
//3. 處理查詢結果
while (resultSet.next()) {
    int column1Value = resultSet.getInt("column1");
    String column2Value = resultSet.getString("column2");
    System.out.println("Column1: " + column1Value + ", Column2: " + column2Value);
}
//4. 關閉資源
resultSet.close();
preparedStatement.close();
connection.close();

上面的方式會頻繁的建立資料庫連線,在比較久遠的 JSP 頁面中會偶爾使用,現在普遍使用 JDBC 連線池。

JDBC 連線池有一個標準的資料來源介面javax.sql.DataSource,這個類位於 Java 標準庫中。

public interface DataSource  extends CommonDataSource, Wrapper {

  Connection getConnection() throws SQLException;

  Connection getConnection(String username, String password) throws SQLException;
}

常用的 JDBC 連線池有:

  • HikariCP
  • C3P0
  • Druid

Druid(阿里巴巴資料庫連線池)是一個開源的資料庫連線池庫,它提供了強大的資料庫連線池管理和監控功能。

1、設定Druid資料來源

DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl("jdbc:mysql://localhost:3306/mydatabase");
dataSource.setUsername("yourusername");
dataSource.setPassword("yourpassword");
dataSource.setInitialSize(5); // 初始連線池大小
dataSource.setMinIdle(5); // 最小空閒連線數
dataSource.setMaxActive(20); // 最大活動連線數
dataSource.setValidationQuery("select 1 from dual");  // 心跳的 Query
dataSource.setMaxWait(60000); // 最大等待時間
dataSource.setTestOnBorrow(true); // 驗證連線是否有效

2、使用資料庫連線

Connection connection = dataSource.getConnection();
//使用連線執行資料庫操作
// TODO 業務操作
// 使用後關閉連線連線
connection.close();

3、關閉資料來源

dataSource.close();

3 連線池 Druid 實現原理

我們學習資料來源的實現,可以從如下五個核心角度分析:

  • 初始化
  • 建立連線
  • 回收連線
  • 歸還連線
  • 銷燬連線

3.1 初始化

首先我們檢視資料來源實現「獲取連線」的介面截圖,初始化可以主動被動兩種方式。

主從是指顯示的呼叫 init 方法,而

呼叫getConnection方法時,返回的物件是連線介面的封裝類 DruidConnectionHolder

在初始化方法內,資料來源建立三個連線池陣列 。

  • connections:用於存放能獲取的連線物件。

  • evictConnections:用於存放需要丟棄的連線物件。

  • keepAliveConnections:用於存放需要保活的連線物件。

初始化階段,需要進行連線池的預熱:也就是需要按照設定首先建立一定數量的連線,並放入到池子裡,這樣應用在需要獲取連線的候,可以直接從池子裡獲取。

資料來源「預熱」分為同步非同步兩種方式 ,見下圖:

從上圖,我們可以看到同步建立連線時,是原生 JDBC 建立連線後,直接放入到 connections 陣列物件裡。

非同步建立執行緒需要初始化 createScheduler , 但預設並沒有設定。

資料來源預熱之後,啟動了兩個任務執行緒:建立連線銷燬連線

3.2 建立連線

這一節,我們重點學習 Druid 資料來源如何建立連線

CreateConnectionThread 本質是一個單執行緒在死迴圈中通過 condition 等待,被其他執行緒喚醒 ,並實現建立資料庫連線邏輯。

筆者將 run 方法做了適當簡化,當滿足了條件之後,才建立資料庫連線 :

  • 必須存線上程等待,才建立連線
  • 防止建立超過最大連線數 maxAcitve

建立完連線物件 PhysicalConnectionInfo 之後,需要儲存到 Connections 陣列裡,並喚醒到其他的執行緒,這樣就可以從池子裡獲取連線。

3.3 獲取連線

我們詳細解析了建立連線的過程,接下來就是應用如何獲取連線的過程。

DruidDataSource#getConnection 方法會呼叫到 DruidDataSource#getConnectionDirect 方法來獲取連線,實現如下所示。

核心流程是

1、在 for 迴圈內,首先呼叫 getConnectionDirect內,呼叫getConnectionInternal 從池子裡獲取連線物件;

2、獲取連線後,需要根據 testOnBorrowtestWhileIdle 引數設定判斷是否需要檢測連線的有效性;

3、最後假如需要判斷連線是否有洩露,則設定 removeAbandoned 來關閉長時間不適用的連線,該功能不建議再生產環境中使用,僅用於連線洩露檢測診斷。

接下來進入獲取連線的重點:getConnectionInternal 方法如何從池子裡獲取連線。

getConnectionInternal()方法中拿到連線的方式有三種:

  1. 直接建立連線(預設設定不會執行)

    需要設定定時執行緒池 createScheduler,當連線池已經沒有可用連線,且當前借出的連線數未達到允許的最大連線數,且當前沒有其它執行緒在建立連線 ;

  2. pollLast 方法:從池中拿連線,並最多等待 maxWait 的時間,需要設定了maxWait

pollLast 方法的核心是:死迴圈內部,通過 Condition 物件 notEmpty 的 awaitNanos 方法執行等待,若池子中有連線,將最後一個連線取出,並將最後一個陣列元素置為空。

  1. takeLast 方法:從池中拿連線,並一直等待直到拿到連線。

和 pollLast 方法不同,首先方法體內部並沒有死迴圈,通過 Condition 物件 notEmpty 的 await 方法等待,直到池子中有連線,將最後一個連線取出,並將最後一個陣列元素置為空。

3.4 歸還連線

DruidDataSource 連線池中,每一個物理連線都會被包裝成DruidConnectionHolder,在提供給應用執行緒前,還會將 DruidConnectionHolder 包裝成 DruidPooledConnection

原生的 JDBC 操作, 每次執行完業務操作之後,會執行關閉連線,對於連線池來講,就是歸還連線,也就是將連線放回連線池

下圖展示了 DruidPooledConnectionclose 方法 :

在關閉方法中,我們重點關注 recycle 回收連線方法。

我們可以簡單的理解:將連線放到 connections 陣列的 poolingCount 位置,並將其自增,然後通過 Condition 物件 notEmpty 喚醒等待獲取連線的一個應用程式。

3.5 銷燬連線

DruidDataSource 連線的銷燬 DestroyConnectionThread 執行緒完成 :

從定時任務(死迴圈)每隔 timeBetweenEvictionRunsMillis 執行一次,我們重點關注destroyTaskrun方法。

destroyTaskrun方法 會呼叫DruidDataSource#shrink方法來根據設定的條件來判斷出需要銷燬和保活的連線。

核心流程:

1、遍歷連線池陣列 connections

​ 內部分別判斷這些連線是需要銷燬還是需要保活 ,並分別加入到對應的容器陣列裡。

2、銷燬場景

  • 空閒時間idleMillis >= 允許的最小空閒時間 minEvictableIdleTimeMillis
  • 空閒時間idleMillis >= 允許的最大空閒時間 maxEvictableIdleTimeMillis

3、保活場景

  • 發生了致命錯誤(onFatalError == true)且致命錯誤發生時間(lastFatalErrorTimeMillis)在連線建立時間之後
  • 如果開啟了保活機制,且連線空閒時間大於等於了保活間隔時間

4、銷燬連線

​ 遍歷陣列 evictConnections 所有的連線,並逐一銷燬 。

5、保活連線

​ 遍歷陣列 keepAliveConnections 所有的連線,對連線進行驗證 ,驗證失敗,則關閉連線,否則加鎖,重新加入到連線池中。

4 保證連線有效

本節,我們講解如何合理的設定引數保證資料庫連線有效。

很多同學都會遇到一個問題:「長時間不進行資料庫讀寫操作之後,第一次請求資料庫,資料庫會報錯,但第二次就正常了。"

那是因為資料庫為了節省資源,會關閉掉長期沒有讀寫的連線

筆者第一次使用 Druid 時就遇到過這樣的問題,有興趣的同學可以看看筆者這篇文章:

https://www.javayong.cn/codelife/runningforcode.html

下圖展示了 Druid 資料來源設定樣例:

我們簡單梳理下 Druid 的保證連線有效有哪些策略:

1、銷燬連線執行緒定時檢測所有的連線,關閉空閒時間過大的連線 ,假如設定了保活引數,那麼會繼續維護待保活的連線;

2、應用每次從資料來源中獲取連線時候,會根據testOnBorrowtestWhileIdle引數檢測連線的有效性。

因此,我們需要重點設定如下的引數:

A、timeBetweenEvictionRunsMillis 引數:間隔多久檢測一次空閒連線是否有效。

B、testWhileIdle 引數:啟空閒連線的檢測,強烈建議設定為 true 。

C、minEvictableIdleTimeMillis 引數:連線池中連線最大空閒時間(毫秒),連線數 > minIdle && 空閒時間 > minEvictableIdleTimeMillis 。

D、maxEvictableIdleTimeMillis 引數:連線池中連線最大空閒時間,空閒時間 > maxEvictableIdleTimeMillis,不管連線池中的連線數是否小於最小連線數 。

E、testOnBorrow 引數:開啟連線的檢測,獲取連線時檢測是否有效,假如設定為 true ,可以最大程度的保證連線的可靠性,但效能會變很差 。

筆者建議在設定這些引數時,和 DBA、架構師做好提前溝通,每個公司的資料庫設定策略並不相同,假如資料庫設定連線存活時間很短,那麼就需要適當減少空閒連線檢測間隔,並調低最大和最小空閒時間。

5 總結

這篇文章,筆者整理了資料庫連線池的知識點。

1、連線池的優點:資源重用、提高效能、優化資源分配、連線管理;

2、JDBC 連線池:實現資料來源介面javax.sql.DataSource,這個類位於 Java 標準庫;

3、連線池 Druid 實現原理

  • 核心方法:初始化、建立連線、獲取連線、歸還連線、銷燬連線。
  • 儲存容器:連線池陣列、銷燬連線陣列、保活連線陣列。
  • 執行緒模型:獨立的建立連線執行緒和銷燬連線執行緒。
  • 鎖機制:在建立連線、獲取連線時,都會加鎖,通過兩個 Condition 物件 emptynotEmpty 分別控制建立連線執行緒和獲取連線執行緒的等待和喚醒。

資料庫連線池、執行緒池都是物件池的思想。物件池是一種設計模式,用於管理可重複使用的物件,以減少物件的建立和銷燬開銷。

筆者會在接下來的文章裡為大家詳解:

  1. 如何使用池化框架 Commons Pool
  2. Netty 如何實現簡單的連線池。

參考文章:

https://segmentfault.com/a/1190000043208041

https://blog.csdn.net/weixin_43790613/article/details/133940617

https://blog.csdn.net/yaomingyang/article/details/123145662


如果我的文章對你有所幫助,還請幫忙點贊、在看、轉發一下,你的支援會激勵我輸出更高質量的文章,非常感謝!