在 Spring Boot 專案中,資料庫連線池已經成為標配,然而,我曾經遇到過不少連線池異常導致業務錯誤的事故。很多經驗豐富的工程師也可能不小心在這方面出現問題。
在這篇文章中,我們將探討資料庫連線池,深入解析其實現機制,以便更好地理解和規避潛在的風險。
1 為什麼需要連線池
假如沒有連線池,我們運算元據庫的流程如下:
- 應用程式使用資料庫驅動建立和資料庫的 TCP 連線 ;
- 使用者進行身份驗證 ;
- 身份驗證透過,應用進行讀寫資料庫操作 ;
- 操作結束後,關閉 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、獲取連線後,需要根據 testOnBorrow
、testWhileIdle
引數配置判斷是否需要檢測連線的有效性;
3、最後假如需要判斷連線是否有洩露,則配置 removeAbandoned
來關閉長時間不適用的連線,該功能不建議再生產環境中使用,僅用於連線洩露檢測診斷。
接下來進入獲取連線的重點:getConnectionInternal
方法如何從池子裡獲取連線。
getConnectionInternal()
方法中拿到連線的方式有三種:
-
直接建立連線(預設配置不會執行)
需要配置定時執行緒池
createScheduler
,當連線池已經沒有可用連線,且當前借出的連線數未達到允許的最大連線數,且當前沒有其它執行緒在建立連線 ; -
pollLast 方法:從池中拿連線,並最多等待 maxWait 的時間,需要設定了maxWait;
pollLast 方法的核心是:死迴圈內部,透過 Condition 物件 notEmpty 的 awaitNanos 方法執行等待,若池子中有連線,將最後一個連線取出,並將最後一個陣列元素置為空。
- takeLast 方法:從池中拿連線,並一直等待直到拿到連線。
和 pollLast 方法不同,首先方法體內部並沒有死迴圈,透過 Condition 物件 notEmpty 的 await 方法等待,直到池子中有連線,將最後一個連線取出,並將最後一個陣列元素置為空。
3.4 歸還連線
DruidDataSource
連線池中,每一個物理連線都會被包裝成DruidConnectionHolder
,在提供給應用執行緒前,還會將 DruidConnectionHolder
包裝成 DruidPooledConnection
。
原生的 JDBC 操作, 每次執行完業務操作之後,會執行關閉連線,對於連線池來講,就是歸還連線,也就是將連線放回連線池。
下圖展示了 DruidPooledConnection
的 close 方法 :
在關閉方法中,我們重點關注 recycle
回收連線方法。
我們可以簡單的理解:將連線放到 connections 陣列的 poolingCount 位置,並將其自增,然後透過 Condition 物件 notEmpty 喚醒等待獲取連線的一個應用程式。
3.5 銷燬連線
DruidDataSource
連線的銷燬 DestroyConnectionThread
執行緒完成 :
從定時任務(死迴圈)每隔 timeBetweenEvictionRunsMillis
執行一次,我們重點關注destroyTask
的run
方法。
destroyTask
的run
方法 會呼叫DruidDataSource#shrink
方法來根據設定的條件來判斷出需要銷燬和保活的連線。
核心流程:
1、遍歷連線池陣列 connections:
內部分別判斷這些連線是需要銷燬還是需要保活 ,並分別加入到對應的容器陣列裡。
2、銷燬場景:
- 空閒時間
idleMillis
>= 允許的最小空閒時間minEvictableIdleTimeMillis
- 空閒時間
idleMillis
>= 允許的最大空閒時間maxEvictableIdleTimeMillis
3、保活場景:
- 發生了致命錯誤(onFatalError == true)且致命錯誤發生時間(
lastFatalErrorTimeMillis
)在連線建立時間之後 - 如果開啟了保活機制,且連線空閒時間大於等於了保活間隔時間
4、銷燬連線:
遍歷陣列 evictConnections 所有的連線,並逐一銷燬 。
5、保活連線:
遍歷陣列 keepAliveConnections 所有的連線,對連線進行驗證 ,驗證失敗,則關閉連線,否則加鎖,重新加入到連線池中。
4 保證連線有效
本節,我們講解如何合理的配置引數保證資料庫連線有效。
很多同學都會遇到一個問題:“長時間不進行資料庫讀寫操作之後,第一次請求資料庫,資料庫會報錯,但第二次就正常了。"
那是因為資料庫為了節省資源,會關閉掉長期沒有讀寫的連線。
筆者第一次使用 Druid 時就遇到過這樣的問題,有興趣的同學可以看看筆者這篇文章:
下圖展示了 Druid 資料來源配置樣例:
我們簡單梳理下 Druid 的保證連線有效有哪些策略:
1、銷燬連線執行緒定時檢測所有的連線,關閉空閒時間過大的連線 ,假如配置了保活引數,那麼會繼續維護待保活的連線;
2、應用每次從資料來源中獲取連線時候,會根據testOnBorrow
、testWhileIdle
引數檢測連線的有效性。
因此,我們需要重點配置如下的引數:
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 物件 empty 、notEmpty 分別控制建立連線執行緒和獲取連線執行緒的等待和喚醒。
資料庫連線池、執行緒池都是物件池的思想。物件池是一種設計模式,用於管理可重複使用的物件,以減少物件的建立和銷燬開銷。
筆者會在接下來的文章裡為大家詳解:
- 如何使用池化框架 Commons Pool ;
- Netty 如何實現簡單的連線池。
參考文章:
https://segmentfault.com/a/1190000043208041
https://blog.csdn.net/weixin_43790613/article/details/133940617
如果我的文章對你有所幫助,還請幫忙點贊、在看、轉發一下,你的支援會激勵我輸出更高質量的文章,非常感謝!