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