再有人問你數(shù)據(jù)庫連接池 Druid 的原理,這篇文章甩給他!
SpringBoot項目中,數(shù)據(jù)庫連接池已經(jīng)成為標配,然而,我曾經(jīng)遇到過不少連接池異常導(dǎo)致業(yè)務(wù)錯誤的事故。很多經(jīng)驗豐富的工程師也可能不小心在這方面出現(xiàn)問題。
在這篇文章中,我們將探討數(shù)據(jù)庫連接池,深入解析其實現(xiàn)機制,以便更好地理解和規(guī)避潛在的風(fēng)險。
圖片
1 為什么需要連接池
假如沒有連接池,我們操作數(shù)據(jù)庫的流程如下:
- 應(yīng)用程序使用數(shù)據(jù)庫驅(qū)動建立和數(shù)據(jù)庫的 TCP 連接 ;
- 用戶進行身份驗證 ;
- 身份驗證通過,應(yīng)用進行讀寫數(shù)據(jù)庫操作 ;
- 操作結(jié)束后,關(guān)閉 TCP 連接 。
創(chuàng)建數(shù)據(jù)庫連接是一個比較昂貴的操作,若同時有幾百人甚至幾千人在線,頻繁地進行連接操作將占用更多的系統(tǒng)資源,但數(shù)據(jù)庫支持的連接數(shù)是有限的,創(chuàng)建大量的連接可能會導(dǎo)致數(shù)據(jù)庫僵死。
當(dāng)我們有了連接池,應(yīng)用程序啟動時就預(yù)先建立多個數(shù)據(jù)庫連接對象,然后將連接對象保存到連接池中。當(dāng)客戶請求到來時,從池中取出一個連接對象為客戶服務(wù)。當(dāng)請求完成時,客戶程序調(diào)用關(guān)閉方法,將連接對象放回池中。
圖片
相比之下,連接池的優(yōu)點顯而易見:
1、資源重用:
因為數(shù)據(jù)庫連接可以重用,避免了頻繁創(chuàng)建,釋放連接引起的大量性能開銷,同時也增加了系統(tǒng)運行環(huán)境的平穩(wěn)性。
2、提高性能
當(dāng)業(yè)務(wù)請求時,因為數(shù)據(jù)庫連接在初始化時已經(jīng)被創(chuàng)建,可以立即使用,而不需要等待連接的建立,減少了響應(yīng)時間。
3、優(yōu)化資源分配
對于多應(yīng)用共享同一數(shù)據(jù)庫的系統(tǒng)而言,可在應(yīng)用層通過數(shù)據(jù)庫連接池的配置,實現(xiàn)某一應(yīng)用最大可用數(shù)據(jù)庫連接數(shù)的限制,避免某一應(yīng)用獨占所有的數(shù)據(jù)庫資源。
4、連接管理
數(shù)據(jù)庫連接池實現(xiàn)中,可根據(jù)預(yù)先的占用超時設(shè)定,強制回收被占用連接,從而避免了常規(guī)數(shù)據(jù)庫連接操作中可能出現(xiàn)的資源泄露。
2 JDBC 連接池
下面的代碼展示了 JDBC 操作數(shù)據(jù)庫的流程 :
//1. 連接到數(shù)據(jù)庫
Connection connection = DriverManager.getConnection(jdbcUrl, username, password);
//2. 執(zhí)行SQL查詢
String sqlQuery = "SELECT * FROM mytable WHERE column1 = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sqlQuery);
preparedStatement.setString(1, "somevalue");
resultSet = preparedStatement.executeQuery();
//3. 處理查詢結(jié)果
while (resultSet.next()) {
int column1Value = resultSet.getInt("column1");
String column2Value = resultSet.getString("column2");
System.out.println("Column1: " + column1Value + ", Column2: " + column2Value);
}
//4. 關(guān)閉資源
resultSet.close();
preparedStatement.close();
connection.close();
上面的方式會頻繁的創(chuàng)建數(shù)據(jù)庫連接,在比較久遠的 JSP 頁面中會偶爾使用,現(xiàn)在普遍使用 JDBC 連接池。
JDBC 連接池有一個標準的數(shù)據(jù)源接口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(阿里巴巴數(shù)據(jù)庫連接池)是一個開源的數(shù)據(jù)庫連接池庫,它提供了強大的數(shù)據(jù)庫連接池管理和監(jiān)控功能。
1)配置Druid數(shù)據(jù)源
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl("jdbc:mysql://localhost:3306/mydatabase");
dataSource.setUsername("yourusername");
dataSource.setPassword("yourpassword");
dataSource.setInitialSize(5); // 初始連接池大小
dataSource.setMinIdle(5); // 最小空閑連接數(shù)
dataSource.setMaxActive(20); // 最大活動連接數(shù)
dataSource.setValidationQuery("select 1 from dual"); // 心跳的 Query
dataSource.setMaxWait(60000); // 最大等待時間
dataSource.setTestOnBorrow(true); // 驗證連接是否有效
2)使用數(shù)據(jù)庫連接
Connection connection = dataSource.getConnection();
//使用連接執(zhí)行數(shù)據(jù)庫操作
// TODO 業(yè)務(wù)操作
// 使用后關(guān)閉連接連接
connection.close();
3)關(guān)閉數(shù)據(jù)源
dataSource.close();
3 連接池 Druid 實現(xiàn)原理
我們學(xué)習(xí)數(shù)據(jù)源的實現(xiàn),可以從如下五個核心角度分析:
- 初始化
- 創(chuàng)建連接
- 回收連接
- 歸還連接
- 銷毀連接
3.1 初始化
首先我們查看數(shù)據(jù)源實現(xiàn)「獲取連接」的接口,初始化可以主動和被動兩種方式。
主從是指顯示的調(diào)用 init 方法,而被動是指獲取連接時完成初始化。
圖片
調(diào)用getConnection方法時,返回的對象是連接接口的封裝類 DruidConnectionHolder 。
在初始化方法內(nèi),數(shù)據(jù)源創(chuàng)建三個連接池數(shù)組 ,他們分別是:
圖片
- connections:用于存放能獲取的連接對象。
- evictConnections:用于存放需要丟棄的連接對象。
- keepAliveConnections:用于存放需要保活的連接對象。
初始化階段,需要進行連接池的「預(yù)熱」:也就是需要按照配置首先創(chuàng)建一定數(shù)量的連接,并放入到池子里,這樣應(yīng)用在需要獲取連接的候,可以直接從池子里獲取。
數(shù)據(jù)源「預(yù)熱」分為同步和異步兩種方式 ,見下圖:
圖片
從上圖,我們可以看到同步創(chuàng)建連接時,是原生 JDBC 創(chuàng)建連接后,直接放入到 connections 數(shù)組對象里。
異步創(chuàng)建線程需要初始化 createScheduler , 但默認并沒有配置。
數(shù)據(jù)源預(yù)熱之后,啟動了兩個任務(wù)線程:創(chuàng)建連接線程和銷毀連接線程。
圖片
3.2 創(chuàng)建連接
這一節(jié),我們重點學(xué)習(xí) Druid 數(shù)據(jù)源如何創(chuàng)建連接。
CreateConnectionThread 本質(zhì)是一個單線程在死循環(huán)中通過 condition 等待,被其他線程喚醒 ,并實現(xiàn)創(chuàng)建數(shù)據(jù)庫連接邏輯。
圖片
筆者將 run 方法做了適當(dāng)簡化,當(dāng)滿足了條件之后,才創(chuàng)建數(shù)據(jù)庫連接 :
- 必須存在線程等待,才創(chuàng)建連接 。
- 防止創(chuàng)建超過最大連接數(shù) maxAcitve 。
創(chuàng)建完連接對象 PhysicalConnectionInfo 之后,需要保存到 Connections 數(shù)組里,并喚醒到其他的線程,這樣就可以從池子里獲取連接。
圖片
3.3 獲取連接
我們詳細解析了創(chuàng)建連接的過程,接下來就是應(yīng)用如何獲取連接的過程。
DruidDataSource#getConnection 方法會調(diào)用到 DruidDataSource#getConnectionDirect 方法來獲取連接,實現(xiàn)如下所示。
圖片
核心流程是
1)在 for 循環(huán)內(nèi),首先調(diào)用 getConnectionDirect內(nèi),調(diào)用getConnectionInternal 從池子里獲取連接對象;
2)獲取連接后,需要根據(jù) testOnBorrow 、testWhileIdle 參數(shù)配置判斷是否需要檢測連接的有效性;
3)最后假如需要判斷連接是否有泄露,則配置 removeAbandoned 來關(guān)閉長時間不適用的連接,該功能不建議再生產(chǎn)環(huán)境中使用,僅用于連接泄露檢測診斷。
接下來進入獲取連接的重點:getConnectionInternal 方法如何從池子里獲取連接。
圖片
getConnectionInternal()方法中拿到連接的方式有三種:
- 直接創(chuàng)建連接(默認配置不會執(zhí)行)需要配置定時線程池 createScheduler,當(dāng)連接池已經(jīng)沒有可用連接,且當(dāng)前借出的連接數(shù)未達到允許的最大連接數(shù),且當(dāng)前沒有其它線程在創(chuàng)建連接 ;
- pollLast 方法:從池中拿連接,并最多等待 maxWait 的時間,需要設(shè)置了maxWait;
圖片
pollLast 方法的核心是:死循環(huán)內(nèi)部,通過 Condition 對象 notEmpty 的 awaitNanos 方法執(zhí)行等待,若池子中有連接,將最后一個連接取出,并將最后一個數(shù)組元素置為空。
takeLast 方法:從池中拿連接,并一直等待直到拿到連接。
和 pollLast 方法不同,首先方法體內(nèi)部并沒有死循環(huán),通過 Condition 對象 notEmpty 的 await 方法等待,直到池子中有連接,將最后一個連接取出,并將最后一個數(shù)組元素置為空。
3.4 歸還連接
DruidDataSource 連接池中,每一個物理連接都會被包裝成DruidConnectionHolder,在提供給應(yīng)用線程前,還會將 DruidConnectionHolder 包裝成 DruidPooledConnection。
圖片
原生的 JDBC 操作, 每次執(zhí)行完業(yè)務(wù)操作之后,會執(zhí)行關(guān)閉連接,對于連接池來講,就是歸還連接,也就是將連接放回連接池。
下圖展示了 DruidPooledConnection 的 close 方法 :
圖片
在關(guān)閉方法中,我們重點關(guān)注 recycle 回收連接方法。
圖片
我們可以簡單的理解:將連接放到 connections 數(shù)組的 poolingCount 位置,并將其自增,然后通過 Condition 對象 notEmpty 喚醒等待獲取連接的一個應(yīng)用程序。
3.5 銷毀連接
DruidDataSource 連接的銷毀 DestroyConnectionThread 線程完成 :
圖片
從定時任務(wù)(死循環(huán))每隔 timeBetweenEvictionRunsMillis 執(zhí)行一次,我們重點關(guān)注destroyTask的run方法。
圖片
destroyTask的run方法 會調(diào)用DruidDataSource#shrink方法來根據(jù)設(shè)定的條件來判斷出需要銷毀和?;畹倪B接。
圖片
核心流程:
1)遍歷連接池數(shù)組 connections:
內(nèi)部分別判斷這些連接是需要銷毀還是需要?;?,并分別加入到對應(yīng)的容器數(shù)組里。
2)銷毀場景:
- 空閑時間idleMillis >= 允許的最小空閑時間 minEvictableIdleTimeMillis
- 空閑時間idleMillis >= 允許的最大空閑時間 maxEvictableIdleTimeMillis
3)保活場景:
- 發(fā)生了致命錯誤(onFatalError == true)且致命錯誤發(fā)生時間(lastFatalErrorTimeMillis)在連接建立時間之后
- 如果開啟了保活機制,且連接空閑時間大于等于了?;铋g隔時間
4)銷毀連接:
遍歷數(shù)組 evictConnections 所有的連接,并逐一銷毀 。
5)?;钸B接:
遍歷數(shù)組 keepAliveConnections 所有的連接,對連接進行驗證 ,驗證失敗,則關(guān)閉連接,否則加鎖,重新加入到連接池中。
4 保證連接有效
本節(jié),我們講解如何合理的配置參數(shù)保證數(shù)據(jù)庫連接有效。
很多同學(xué)都會遇到一個問題:“長時間不進行數(shù)據(jù)庫讀寫操作之后,第一次請求數(shù)據(jù)庫,數(shù)據(jù)庫會報錯,但第二次就正常了。"
那是因為數(shù)據(jù)庫為了節(jié)省資源,會關(guān)閉掉長期沒有讀寫的連接。
筆者第一次使用 Druid 時就遇到過這樣的問題,有興趣的同學(xué)可以看看筆者這篇文章:
下圖展示了 Druid 數(shù)據(jù)源配置樣例:
圖片
我們簡單梳理下 Druid 的保證連接有效有哪些策略:
1、銷毀連接線程定時檢測所有的連接,關(guān)閉空閑時間過大的連接 ,假如配置了?;顓?shù),那么會繼續(xù)維護待?;畹倪B接;
2、應(yīng)用每次從數(shù)據(jù)源中獲取連接時候,會根據(jù)testOnBorrow、testWhileIdle參數(shù)檢測連接的有效性。
因此,我們需要重點配置如下的參數(shù):
A、timeBetweenEvictionRunsMillis 參數(shù):間隔多久檢測一次空閑連接是否有效。
B、testWhileIdle 參數(shù):啟空閑連接的檢測,強烈建議設(shè)置為 true 。
C、minEvictableIdleTimeMillis 參數(shù):連接池中連接最大空閑時間(毫秒),連接數(shù) > minIdle && 空閑時間 > minEvictableIdleTimeMillis 。
D、maxEvictableIdleTimeMillis 參數(shù):連接池中連接最大空閑時間,空閑時間 > maxEvictableIdleTimeMillis,不管連接池中的連接數(shù)是否小于最小連接數(shù) 。
E、testOnBorrow 參數(shù):開啟連接的檢測,獲取連接時檢測是否有效,假如設(shè)置為 true ,可以最大程度的保證連接的可靠性,但性能會變很差 。
筆者建議在配置這些參數(shù)時,和 DBA、架構(gòu)師做好提前溝通,每個公司的數(shù)據(jù)庫配置策略并不相同,假如數(shù)據(jù)庫配置連接存活時間很短,那么就需要適當(dāng)減少空閑連接檢測間隔,并調(diào)低最大和最小空閑時間。
5 總結(jié)
這篇文章,筆者整理了數(shù)據(jù)庫連接池的知識點。
1)連接池的優(yōu)點:資源重用、提高性能、優(yōu)化資源分配、連接管理;
2)JDBC 連接池:實現(xiàn)數(shù)據(jù)源接口javax.sql.DataSource,這個類位于 Java 標準庫;
3)連接池 Druid 實現(xiàn)原理:
- 核心方法:初始化、創(chuàng)建連接、獲取連接、歸還連接、銷毀連接。
- 存儲容器:連接池數(shù)組、銷毀連接數(shù)組、保活連接數(shù)組。
- 線程模型:獨立的創(chuàng)建連接線程和銷毀連接線程。
- 鎖機制:在創(chuàng)建連接、獲取連接時,都會加鎖,通過兩個 Condition 對象 empty 、notEmpty 分別控制創(chuàng)建連接線程和獲取連接線程的等待和喚醒。
4)連接池?;畈呗?/p>
配置連接池參數(shù)時,和 DBA、架構(gòu)師做好提前溝通,每個公司的數(shù)據(jù)庫配置策略并不相同,假如數(shù)據(jù)庫配置連接存活時間很短,那么就需要適當(dāng)減少空閑連接檢測間隔,并調(diào)低最大和最小空閑時間。
最后,數(shù)據(jù)庫連接池、線程池都是對象池的思想。對象池是一種設(shè)計模式,用于管理可重復(fù)使用的對象,以減少對象的創(chuàng)建和銷毀開銷。
筆者會在接下來的文章里為大家詳解,敬請期待:
- 如何使用池化框架 Commons Pool ;
- Netty 如何實現(xiàn)簡單的連接池。
參考文章:
https://segmentfault.com/a/1190000043208041
https://blog.csdn.net/weixin_43790613/article/details/133940617