druid
是用於建立和管理鏈接,利用「池」的方式複用鏈接減小資源開銷,和其餘數據源同樣,也具備鏈接數控制、鏈接可靠性測試、鏈接泄露控制、緩存語句等功能,另外,druid
還擴展了監控統計、防護SQL注入等功能。css
本文將包含如下內容(由於篇幅較長,可根據須要選擇閱讀):html
druid
的使用方法(入門案例、JDNI
使用、監控統計、防護SQL注入)druid
的配置參數詳解druid
主要源碼分析其餘鏈接池的內容也能夠參考個人其餘博客:java
源碼詳解系列(四) ------ DBCP2的使用和分析(包括JNDI和JTA支持)mysql
源碼詳解系列(五) ------ C3P0的使用和分析(包括JNDI)git
使用druid
鏈接池獲取鏈接對象,對用戶數據進行簡單的增刪改查(sql
腳本項目中已提供)。github
JDK
:1.8.0_231web
maven
:3.6.1spring
IDE
:eclipse 4.12sql
mysql-connector-java
:8.0.15數據庫
mysql
:5.7 .28
druid
:1.1.20
編寫druid.properties
,設置數據庫鏈接參數和鏈接池基本參數等
經過DruidDataSourceFactory
加載druid.properties
文件,並建立DruidDataSource
對象
經過DruidDataSource
對象得到Connection
對象
使用Connection
對象對用戶表進行增刪改查
項目類型Maven Project,打包方式war(其實jar也能夠,之因此使用war是爲了測試JNDI
)。
這裏引入日誌包,主要爲了看看鏈接池的建立過程,不引入不會有影響的。
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <!-- druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.20</version> </dependency> <!-- mysql驅動 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.15</version> </dependency> <!-- log --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency>
配置文件路徑在resources
目錄下,由於是入門例子,這裏僅給出數據庫鏈接參數和鏈接池基本參數,後面會對全部配置參數進行詳細說明。另外,數據庫sql
腳本也在該目錄下。
固然,咱們也能夠經過啓動參數來進行配置(但這種方式可配置參數會少一些)。
#-------------基本屬性-------------------------------- url=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true username=root password=root #數據源名,當配置多數據源時能夠用於區分。注意,1.0.5版本及更早版本不支持配置該項 #默認"DataSource-" + System.identityHashCode(this) name=zzs001 #若是不配置druid會根據url自動識別dbType,而後選擇相應的driverClassName driverClassName=com.mysql.cj.jdbc.Driver #-------------鏈接池大小相關參數-------------------------------- #初始化時創建物理鏈接的個數 #默認爲0 initialSize=0 #最大鏈接池數量 #默認爲8 maxActive=8 #最小空閒鏈接數量 #默認爲0 minIdle=0 #已過時 #maxIdle #獲取鏈接時最大等待時間,單位毫秒。 #配置了maxWait以後,缺省啓用公平鎖,併發效率會有所降低,若是須要能夠經過配置useUnfairLock屬性爲true使用非公平鎖。 #默認-1,表示無限等待 maxWait=-1
項目中編寫了JDBCUtil
來初始化鏈接池、獲取鏈接、管理事務和釋放資源等,具體參見項目源碼。
路徑:cn.zzs.druid
Properties properties = new Properties(); InputStream in = JDBCUtils.class.getClassLoader().getResourceAsStream("druid.properties"); properties.load(in); DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
這裏以保存用戶爲例,路徑在test目錄下的cn.zzs.druid
。
@Test public void save() throws SQLException { // 建立sql String sql = "insert into demo_user values(null,?,?,?,?,?)"; Connection connection = null; PreparedStatement statement = null; try { // 得到鏈接 connection = JDBCUtils.getConnection(); // 開啓事務設置非自動提交 connection.setAutoCommit(false); // 得到Statement對象 statement = connection.prepareStatement(sql); // 設置參數 statement.setString(1, "zzf003"); statement.setInt(2, 18); statement.setDate(3, new Date(System.currentTimeMillis())); statement.setDate(4, new Date(System.currentTimeMillis())); statement.setBoolean(5, false); // 執行 statement.executeUpdate(); // 提交事務 connection.commit(); } finally { // 釋放資源 JDBCUtils.release(connection, statement, null); } }
JNDI
獲取數據源本文測試使用JNDI
獲取DruidDataSource
對象,選擇使用tomcat 9.0.21
做容器。
若是以前沒有接觸過JNDI
,並不會影響下面例子的理解,其實能夠理解爲像spring
的bean
配置和獲取。
本文在入門例子的基礎上增長如下依賴,由於是web
項目,因此打包方式爲war
:
<dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <version>2.2.1</version> <scope>provided</scope> </dependency>
在webapp
文件下建立目錄META-INF
,並建立context.xml
文件。這裏面的每一個resource
節點都是咱們配置的對象,相似於spring
的bean
節點。其中jdbc/druid-test
能夠當作是這個bean
的id
。
注意,這裏獲取的數據源對象是單例的,若是但願多例,能夠設置singleton="false"
。
<?xml version="1.0" encoding="UTF-8"?> <Context> <Resource name="jdbc/druid-test" factory="com.alibaba.druid.pool.DruidDataSourceFactory" auth="Container" type="javax.sql.DataSource" maxActive="15" initialSize="3" minIdle="3" maxWait="10000" url="jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true" username="root" password="root" filters="mergeStat,log4j" validationQuery="select 1 from dual" /> </Context>
在web-app
節點下配置資源引用,每一個resource-ref
指向了咱們配置好的對象。
<!-- JNDI數據源 --> <resource-ref> <res-ref-name>jdbc/druid-test</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref>
由於須要在web
環境中使用,若是直接建類寫個main
方法測試,會一直報錯的,目前沒找到好的辦法。這裏就簡單地使用jsp
來測試吧。
druid
提供了DruidDataSourceFactory
來支持JNDI
。
<body> <% String jndiName = "java:comp/env/jdbc/druid-test"; InitialContext ic = new InitialContext(); // 獲取JNDI上的ComboPooledDataSource DataSource ds = (DataSource) ic.lookup(jndiName); JDBCUtils.setDataSource(ds); // 建立sql String sql = "select * from demo_user where deleted = false"; Connection connection = null; PreparedStatement statement = null; ResultSet resultSet = null; // 查詢用戶 try { // 得到鏈接 connection = JDBCUtils.getConnection(); // 得到Statement對象 statement = connection.prepareStatement(sql); // 執行 resultSet = statement.executeQuery(); // 遍歷結果集 while(resultSet.next()) { String name = resultSet.getString(2); int age = resultSet.getInt(3); System.err.println("用戶名:" + name + ",年齡:" + age); } } catch(SQLException e) { System.err.println("查詢用戶異常"); } finally { // 釋放資源 JDBCUtils.release(connection, statement, resultSet); } %> </body>
打包項目在tomcat9
上運行,訪問 http://localhost:8080/druid-demo/testJNDI.jsp ,控制檯打印以下內容:
用戶名:zzs001,年齡:18 用戶名:zzs002,年齡:18 用戶名:zzs003,年齡:25 用戶名:zzf001,年齡:26 用戶名:zzf002,年齡:17 用戶名:zzf003,年齡:18
在以上例子基礎上修改。
druid的監控統計功能是經過filter-chain
擴展實現,若是你要打開監控統計功能,配置StatFilter
,以下:
filters=stat
stat是com.alibaba.druid.filter.stat.StatFilter
的別名,別名映射配置信息保存在druid-xxx.jar!/META-INF/druid-filter.properties
。
當你程序中存在沒有參數化的sql執行時,sql統計的效果會很差。好比:
select * from t where id = 1 select * from t where id = 2 select * from t where id = 3
在統計中,顯示爲3條sql,這不是咱們但願要的效果。StatFilter提供合併的功能,可以將這3個SQL合併爲以下的SQL:
select * from t where id = ?
能夠配置StatFilter
的mergeSql
屬性來解決:
#用於設置filter的屬性 #多個參數用";"隔開 connectionProperties=druid.stat.mergeSql=true
StatFilter
支持一種簡化配置方式,和上面的配置等同的。以下:
filters=mergeStat
mergeStat
是的MergeStatFilter
縮寫,咱們看MergeStatFilter
的實現:
public class MergeStatFilter extends StatFilter { public MergeStatFilter() { super.setMergeSql(true); } }
從實現代碼來看,僅僅是一個mergeSql
的缺省值。
StatFilter
屬性slowSqlMillis
用來配置SQL慢的標準,執行時間超過slowSqlMillis
的就是慢。slowSqlMillis
的缺省值爲3000,也就是3秒。
connectionProperties=druid.stat.logSlowSql=true;druid.stat.slowSqlMillis=5000
在上面的配置中,slowSqlMillis被修改成5秒,而且經過日誌輸出執行慢的SQL。
缺省多個DruidDataSource
的監控數據是各自獨立的,在druid-0.2.17版本以後,支持配置公用監控數據,配置參數爲useGlobalDataSourceStat
。例如:
connectionProperties=druid.useGlobalDataSourceStat=true
druid內置提供了一個StatViewServlet
用於展現Druid的統計信息。
這個StatViewServlet
的用途包括:
注意:使用StatViewServlet
,建議使用druid 0.2.6以上版本。
StatViewServlet
是一個標準的javax.servlet.http.HttpServlet
,須要配置在你web應用中的WEB-INF/web.xml
中。
<servlet> <servlet-name>DruidStatView</servlet-name> <servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>DruidStatView</servlet-name> <url-pattern>/druid/*</url-pattern> </servlet-mapping>
根據配置中的url-pattern來訪問內置監控頁面,若是是上面的配置,內置監控頁面的首頁是/druid/index.html
例如: http://localhost:8080/druid-demo/druid/index.html
須要配置Servlet
的 loginUsername
和 loginPassword
這兩個初始參數。
示例以下:
<!-- 配置 Druid 監控信息顯示頁面 --> <servlet> <servlet-name>DruidStatView</servlet-name> <servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class> <init-param> <!-- 容許清空統計數據 --> <param-name>resetEnable</param-name> <param-value>true</param-value> </init-param> <init-param> <!-- 用戶名 --> <param-name>loginUsername</param-name> <param-value>druid</param-value> </init-param> <init-param> <!-- 密碼 --> <param-name>loginPassword</param-name> <param-value>druid</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>DruidStatView</servlet-name> <url-pattern>/druid/*</url-pattern> </servlet-mapping>
StatViewSerlvet
展現出來的監控信息比較敏感,是系統運行的內部狀況,若是你須要作訪問控制,能夠配置allow
和deny
這兩個參數。好比:
<servlet> <servlet-name>DruidStatView</servlet-name> <servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class> <init-param> <param-name>allow</param-name> <param-value>128.242.127.1/24,128.242.128.1</param-value> </init-param> <init-param> <param-name>deny</param-name> <param-value>128.242.127.4</param-value> </init-param> </servlet>
判斷規則:
deny
優先於allow
,若是在deny
列表中,就算在allow
列表中,也會被拒絕。allow
沒有配置或者爲空,則容許全部訪問在StatViewSerlvet
輸出的html頁面中,有一個功能是Reset All
,執行這個操做以後,會致使全部計數器清零,從新計數。你能夠經過配置參數關閉它。
<servlet> <servlet-name>DruidStatView</servlet-name> <servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class> <init-param> <param-name>resetEnable</param-name> <param-value>false</param-value> </init-param> </servlet>
WebStatFilter
用於採集web-jdbc
關聯監控的數據。常常須要排除一些沒必要要的url,好比.js
,/jslib/
等等。配置在init-param
中。好比:
<filter> <filter-name>DruidWebStatFilter</filter-name> <filter-class>com.alibaba.druid.support.http.WebStatFilter</filter-class> <init-param> <param-name>exclusions</param-name> <param-value>*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*</param-value> </init-param> </filter> <filter-mapping> <filter-name>DruidWebStatFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
啓動程度,訪問http://localhost:8080/druid-demo/druid/index.html
,登陸後可見如下頁面,經過該頁面咱們能夠查看數據源配置參數、進行SQL統計和監控,等等:
WallFilter
用於對SQL進行攔截,經過如下配置開啓:
#過濾器 filters=wall,stat
注意,這種配置攔截檢測的時間不在StatFilter
統計的SQL執行時間內。 若是但願StatFilter
統計的SQL執行時間內,則使用以下配置
#過濾器 filters=stat,wall
WallFilter
經常使用參數以下,能夠經過connectionProperties
屬性進行配置:
參數 | 缺省值 | 描述 |
---|---|---|
wall.logViolation | false | 對被認爲是攻擊的SQL進行LOG.error輸出 |
wall.throwException | true | 對被認爲是攻擊的SQL拋出SQLException |
wall.updateAllow | true | 是否容許執行UPDATE語句 |
wall.deleteAllow | true | 是否容許執行DELETE語句 |
wall.insertAllow | true | 是否容許執行INSERT語句 |
wall.selelctAllow | true | 否容許執行SELECT語句 |
wall.multiStatementAllow | false | 是否容許一次執行多條語句,缺省關閉 |
wall.selectLimit | -1 | 配置最大返回行數,若是select語句沒有指定最大返回行數,會自動修改selct添加返回限制 |
wall.updateWhereNoneCheck | false | 檢查UPDATE語句是否無where條件,這是有風險的,但不是SQL注入類型的風險 |
wall.deleteWhereNoneCheck | false | 檢查DELETE語句是否無where條件,這是有風險的,但不是SQL注入類型的風險 |
druid內置提供了四種LogFilter
(Log4jFilter
、Log4j2Filter
、CommonsLogFilter
、Slf4jLogFilter
),用於輸出JDBC執行的日誌。這些Filter
都是Filter-Chain
擴展機制中的Filter
,因此配置方式能夠參考這裏:
#過濾器 filters=log4j
在druid-xxx.jar!/META-INF/druid-filter.properties
文件中描述了這四種Filter的別名:
druid.filters.log4j=com.alibaba.druid.filter.logging.Log4jFilter druid.filters.log4j2=com.alibaba.druid.filter.logging.Log4j2Filter druid.filters.slf4j=com.alibaba.druid.filter.logging.Slf4jLogFilter druid.filters.commonlogging=com.alibaba.druid.filter.logging.CommonsLogFilter druid.filters.commonLogging=com.alibaba.druid.filter.logging.CommonsLogFilter
他們的別名分別是log4j
、log4j2
、slf4j
、commonlogging
和commonLogging
。其中commonlogging
和commonLogging
只是大小寫不一樣。
缺省輸入的日誌信息全面,可是內容比較多,有時候咱們須要定製化配置日誌輸出。
connectionProperties=druid.log.rs=false
相關參數以下,更多參數請參考com.alibaba.druid.filter.logging.LogFilter
:
參數 | 說明 | properties參數 |
---|---|---|
connectionLogEnabled | 全部鏈接相關的日誌 | druid.log.conn |
statementLogEnabled | 全部Statement相關的日誌 | druid.log.stmt |
resultSetLogEnabled | 全部ResultSe相關的日誌 | druid.log.rs |
statementExecutableSqlLogEnable | 全部Statement執行語句相關的日誌 | druid.log.stmt.executableSql |
若是你使用log4j
,能夠經過log4j.properties
文件配置日誌輸出選項,例如:
log4j.logger.druid.sql=warn,stdout log4j.logger.druid.sql.DataSource=warn,stdout log4j.logger.druid.sql.Connection=warn,stdout log4j.logger.druid.sql.Statement=warn,stdout log4j.logger.druid.sql.ResultSet=warn,stdout
參數配置方式
connectionProperties=druid.log.stmt.executableSql=true
使用druid,同一個參數,咱們能夠採用多種方式進行配置,舉個例子:maxActive
(最大鏈接池參數)的配置:
系統屬性通常在啓動參數中設置。經過方式一來配置鏈接池參數的仍是比較少見。
-Ddruid.maxActive=8
這是最多見的一種。
maxActive=8
相比第二種方式,這裏只是加了.druid
前綴。
druid.maxActive=8
connectionProperties
能夠用於配置多個屬性,不一樣屬性使用";"隔開。
connectionProperties=druid.maxActive=8
connectProperties
能夠在方式1、方式三和方式四中存在,具體配置以下:
# 方式一 -Ddruid.connectProperties=druid.maxActive=8 # 方式三:支持多個屬性,不一樣屬性使用";"隔開 druid.connectProperties=druid.maxActive=8 # 方式四 connectionProperties=druid.connectProperties=druid.maxActive=8
這個屬性甚至能夠這樣配(固然應該沒人會這麼作):
druid.connectProperties=druid.connectProperties=druid.connectProperties=druid.connectProperties=druid.maxActive=8
真的是沒完沒了,怎麼會引入connectProperties
這個屬性呢?我以爲這是一個十分失敗的設計,因此本文僅會講前面說的四種。
前面已經講到,同一個參數,咱們有時能夠採用無數種方式來配置。表面上看這樣設計十分人性化,能夠適應不一樣人羣的使用習慣,可是,在我看來,這樣設計很是不利於配置的統一管理,另外,druid的參數配置還存在另外一個問題,先看下這個表格(這裏包含了druid全部的參數,使用時能夠參考):
參數分類 | 參數 | 方式一 | 方式二 | 方式三 | 方式四 |
---|---|---|---|---|---|
基本屬性 | driverClassName | O | O | O | O |
password | O | O | O | O | |
url | O | O | O | O | |
username | O | O | O | O | |
事務相關 | defaultAutoCommit | X | O | X | X |
defaultReadOnly | X | O | X | X | |
defaultTransactionIsolation | X | O | X | X | |
defaultCatalog | X | O | X | X | |
鏈接池大小 | maxActive | O | O | O | O |
maxIdle | X | O | X | X | |
minIdle | O | O | O | O | |
initialSize | O | O | O | O | |
maxWait | O | O | O | O | |
鏈接檢測 | testOnBorrow | O | O | O | O |
testOnReturn | X | O | X | X | |
timeBetweenEvictionRunsMillis | O | O | O | O | |
numTestsPerEvictionRun | X | O | X | X | |
minEvictableIdleTimeMillis | O | O | O | O | |
maxEvictableIdleTimeMillis | O | X | O | O | |
phyTimeoutMillis | O | O | O | O | |
testWhileIdle | O | O | O | O | |
validationQuery | O | O | O | O | |
validationQueryTimeout | X | O | X | X | |
鏈接泄露回收 | removeAbandoned | X | O | X | X |
removeAbandonedTimeout | X | O | X | X | |
logAbandoned | X | O | X | X | |
緩存語句 | poolPreparedStatements | O | O | O | O |
maxOpenPreparedStatements | X | O | X | X | |
maxPoolPreparedStatementPerConnectionSize | O | X | O | O | |
其餘 | initConnectionSqls | O | O | O | O |
init | X | O | X | X | |
asyncInit | O | X | O | O | |
initVariants | O | X | O | O | |
initGlobalVariants | O | X | O | O | |
accessToUnderlyingConnectionAllowed | X | O | X | X | |
exceptionSorter | X | O | X | X | |
exception-sorter-class-name | X | O | X | X | |
name | O | X | O | O | |
notFullTimeoutRetryCount | O | X | O | O | |
maxWaitThreadCount | O | X | O | O | |
failFast | O | X | O | O | |
phyMaxUseCount | O | X | O | O | |
keepAlive | O | X | O | O | |
keepAliveBetweenTimeMillis | O | X | O | O | |
useUnfairLock | O | X | O | O | |
killWhenSocketReadTimeout | O | X | O | O | |
load.spifilter.skip | O | X | O | O | |
cacheServerConfiguration | X | X | X | O | |
過濾器 | filters | O | O | O | O |
clearFiltersEnable | O | X | O | O | |
log.conn | O | X | X | O | |
log.stmt | O | X | X | O | |
log.rs | O | X | X | O | |
log.stmt.executableSql | O | X | X | O | |
timeBetweenLogStatsMillis | O | X | O | O | |
useGlobalDataSourceStat/useGloalDataSourceStat | O | X | O | O | |
resetStatEnable | O | X | O | O | |
stat.sql.MaxSize | O | X | O | O | |
stat.mergeSql | O | X | X | O | |
stat.slowSqlMillis | O | X | X | O | |
stat.logSlowSql | O | X | X | O | |
stat.loggerName | X | X | X | O | |
wall.logViolation | O | X | X | O | |
wall.throwException | O | X | X | O | |
wall.tenantColumn | O | X | X | O | |
wall.updateAllow | O | X | X | O | |
wall.deleteAllow | O | X | X | O | |
wall.insertAllow | O | X | X | O | |
wall.selelctAllow | O | X | X | O | |
wall.multiStatementAllow | O | X | X | O | |
wall.selectLimit | O | X | X | O | |
wall.updateCheckColumns | O | X | X | O | |
wall.updateWhereNoneCheck | O | X | X | O | |
wall.deleteWhereNoneCheck | O | X | X | O |
通常咱們都但願採用一種方式來統一配置這些參數,可是,經過以上表格可知,druid並不存在哪種方式能配置全部參數,也就是說,你不得不採用兩種或兩種以上的配置方式。因此,我認爲,至少在配置方式這一點上,druid是很是失敗的!
經過表格可知,方式二和方式四結合使用,能夠覆蓋全部參數,因此,本文采用的配置策略爲:優先採用方式二配置,配不了再選用方式四。
注意,這裏在url
後面拼接了多個參數用於避免亂碼、時區報錯問題。 補充下,若是不想加入時區的參數,能夠在mysql
命令窗口執行以下命令:set global time_zone='+8:00'
。
#-------------基本屬性-------------------------------- url=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true username=root password=root #數據源名,當配置多數據源時能夠用於區分。注意,1.0.5版本及更早版本不支持配置該項 #默認"DataSource-" + System.identityHashCode(this) name=zzs001 #若是不配置druid會根據url自動識別dbType,而後選擇相應的driverClassName driverClassName=com.mysql.cj.jdbc.Driver
這幾個參數都比較經常使用,具體設置多少需根據項目調整。
#-------------鏈接池大小相關參數-------------------------------- #初始化時創建物理鏈接的個數 #默認爲0 initialSize=0 #最大鏈接池數量 #默認爲8 maxActive=8 #最小空閒鏈接數量 #默認爲0 minIdle=0 #已過時 #maxIdle #獲取鏈接時最大等待時間,單位毫秒。 #配置了maxWait以後,缺省啓用公平鎖,併發效率會有所降低,若是須要能夠經過配置useUnfairLock屬性爲true使用非公平鎖。 #默認-1,表示無限等待 maxWait=-1
針對鏈接失效的問題,建議開啓空閒鏈接測試,而不建議開啓借出測試(從性能考慮),另外,開啓鏈接測試時,必須配置validationQuery
。
#-------------鏈接檢測狀況-------------------------------- #用來檢測鏈接是否有效的sql,要求是一個查詢語句,經常使用select 'x'。 #若是validationQuery爲null,testOnBorrow、testOnReturn、testWhileIdle都不會起做用。 #默認爲空 validationQuery=select 1 from dual #檢測鏈接是否有效的超時時間,單位:秒。 #底層調用jdbc Statement對象的void setQueryTimeout(int seconds)方法 #默認-1 validationQueryTimeout=-1 #申請鏈接時執行validationQuery檢測鏈接是否有效,作了這個配置會下降性能。 #默認爲false testOnBorrow=false #歸還鏈接時執行validationQuery檢測鏈接是否有效,作了這個配置會下降性能。 #默認爲false testOnReturn=false #申請鏈接的時候檢測,若是空閒時間大於timeBetweenEvictionRunsMillis,執行validationQuery檢測鏈接是否有效。 #建議配置爲true,不影響性能,而且保證安全性。 #默認爲true testWhileIdle=true #有兩個含義: #1) Destroy線程會檢測鏈接的間隔時間,若是鏈接空閒時間大於等於minEvictableIdleTimeMillis則關閉物理鏈接。 #2) testWhileIdle的判斷依據,詳細看testWhileIdle屬性的說明 #默認1000*60 timeBetweenEvictionRunsMillis=-1 #再也不使用,一個DruidDataSource只支持一個EvictionRun #numTestsPerEvictionRun=3 #鏈接保持空閒而不被驅逐的最小時間。 #默認值1000*60*30 = 30分鐘 minEvictableIdleTimeMillis=1800000
針對大部分數據庫而言,開啓緩存語句能夠有效提升性能,可是在myslq下建議關閉。
#-------------緩存語句-------------------------------- #是否緩存preparedStatement,也就是PSCache。 #PSCache對支持遊標的數據庫性能提高巨大,好比說oracle。在mysql下建議關閉 #默認爲false poolPreparedStatements=false #PSCache的最大個數。 #要啓用PSCache,必須配置大於0,當大於0時,poolPreparedStatements自動觸發修改成true。 #在Druid中,不會存在Oracle下PSCache佔用內存過多的問題,能夠把這個數值配置大一些,好比說100 #默認爲10 maxOpenPreparedStatements=10
建議保留默認就行。
#-------------事務相關的屬性-------------------------------- #鏈接池建立的鏈接的默認的auto-commit狀態 #默認爲空,由驅動決定 defaultAutoCommit=true #鏈接池建立的鏈接的默認的read-only狀態。 #默認值爲空,由驅動決定 defaultReadOnly=false #鏈接池建立的鏈接的默認的TransactionIsolation狀態 #可用值爲下列之一:NONE,READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE #默認值爲空,由驅動決定 defaultTransactionIsolation=REPEATABLE_READ #鏈接池建立的鏈接的默認的數據庫名 defaultCatalog=github_demo
#-------------鏈接泄漏回收參數-------------------------------- #當未使用的時間超過removeAbandonedTimeout時,是否視該鏈接爲泄露鏈接並刪除 #默認爲false removeAbandoned=false #泄露的鏈接能夠被刪除的超時值, 單位毫秒 #默認爲300*1000 removeAbandonedTimeoutMillis=300*1000 #標記當Statement或鏈接被泄露時是否打印程序的stack traces日誌。 #默認爲false logAbandoned=true #鏈接最大存活時間 #默認-1 #phyTimeoutMillis=-1
#-------------過濾器-------------------------------- #屬性類型是字符串,經過別名的方式配置擴展插件,經常使用的插件有: #別名映射配置信息保存在druid-xxx.jar!/META-INF/druid-filter.properties #監控統計用的filter:stat(mergeStat能夠合併sql) #日誌用的filter:log4j #防護sql注入的filter:wall filters=log4j,wall,mergeStat #用於設置filter、exceptionSorter、validConnectionChecker等的屬性 #多個參數用";"隔開 connectionProperties=druid.useGlobalDataSourceStat=true;druid.stat.logSlowSql=true;druid.stat.slowSqlMillis=5000
#-------------其餘-------------------------------- #控制PoolGuard是否允許獲取底層鏈接 #默認爲false accessToUnderlyingConnectionAllowed=false #當數據庫拋出一些不可恢復的異常時,拋棄鏈接 #根據dbType自動識別 #exceptionSorter #exception-sorter-class-name= #物理鏈接初始化的時候執行的sql #initConnectionSqls= #是否建立數據源時就初始化鏈接池 init=true
看過druid的源碼就會發現,相比其餘DBCP和C3P0,druid有如下特色:
CountDownLatch
、ReentrantLock
、AtomicLongFieldUpdater
、Condition
等,也就是說,在分析druid源碼以前,最好先學習下這些技術;DruidDataSource
裏面。另外,在對類或接口的抽象上,我的感受,druid不是很「面向對象」,有的接口或類的方法很難統一成某種對象的行爲,因此,本文不會去關注類的設計,更多地將分析一些重要功能的實現。注意:考慮篇幅和可讀性,如下代碼通過刪減,僅保留所需部分。
前面已經講過,druid爲咱們提供了「無數」種方式來配置參數,這裏我再補充下不一樣配置方式的加載順序(固然,只會涉及到四種方式)。
當咱們使用調用DruidDataSourceFactory.createDataSource(Properties)
時,會加載配置來給對應的屬性賦值,另外,這個過程還會根據配置去建立對應的過濾器。不一樣配置方式加載時機不一樣,後者會覆蓋已存在的相同參數,如圖所示。
<img src="https://img2018.cnblogs.com/blog/1731892/202001/1731892-20200110135916270-964950053.png" alt="druid不一樣配置方式的加載順序" style="zoom:80%;" />
這裏先來介紹下DruidDataSource
這個類:
圖中我只列出了幾個重要的屬性,這幾個屬性沒有理解好,後面的源碼很難看得進去。
類名 | 描述 |
---|---|
ExceptionSorter | 用於判斷SQLException對象是否致命異常 |
ValidConnectionChecker | 用於校驗指定鏈接對象是否有效 |
CreateConnectionThread | DruidDataSource的內部類,用於異步建立鏈接對象 |
notEmpty | 調用notEmpty.await()時,當前線程進入等待;當鏈接建立完成或者回收了鏈接,會調用notEmpty.signal()時,將等待線程喚醒; |
empty | 調用empty.await()時,CreateConnectionThread進入等待;調用empty.signal()時,CreateConnectionThread被喚醒,並進入建立鏈接; |
DestroyConnectionThread | DruidDataSource的內部類,用於異步檢驗鏈接對象,包括校驗空閒鏈接的phyTimeoutMillis、minEvictableIdleTimeMillis,以及校驗借出鏈接的removeAbandonedTimeoutMillis |
LogStatsThread | DruidDataSource的內部類,用於異步記錄統計信息 |
connections | 用於存放全部鏈接對象 |
evictConnections | 用於存放須要丟棄的鏈接對象 |
keepAliveConnections | 用於存放須要keepAlive的鏈接對象 |
activeConnections | 用於存放須要進行removeAbandoned的鏈接對象 |
poolingCount | 空閒鏈接對象的數量 |
activeCount | 借出鏈接對象的數量 |
DruidDataSource
的初始化時機是可選的,當咱們設置init=true
時,在createDataSource
時就會調用DataSource.init()
方法進行初始化,不然,只會在getConnection
時再進行初始化。數據源初始化主要邏輯在DataSource.init()
這個方法,能夠歸納爲如下步驟:
initStackTrace
、id
、xxIdSeed
、dbTyp
、driver
、dataSourceStat
、connections
、evictConnections
、keepAliveConnections
等屬性maxActive
、minIdle
、initialSize
、timeBetweenLogStatsMillis
、useGlobalDataSourceStat
、maxEvictableIdleTimeMillis
、minEvictableIdleTimeMillis
、validationQuery
等配置是否合法ExceptionSorter
、ValidConnectionChecker
、JdbcDataSourceStat
initialSize
數量的鏈接logStatsThread
、createConnectionThread
和destroyConnectionThread
createConnectionThread
和destroyConnectionThread
線程run後再繼續執行MBean
,用於支持JMXkeepAlive
,通知createConnectionThread
建立鏈接對象這個方法差很少200行,考慮篇幅,我刪減了部份內容。
druid數據源初始化採用的是ReentrantLock
,以下:
final ReentrantLock lock = this.lock; try { // 加鎖 lock.lockInterruptibly(); } catch (InterruptedException e) { throw new SQLException("interrupt", e); } boolean init = false; try { // do something } finally { inited = true; // 解鎖 lock.unlock(); }
注意,如下步驟均在這個鎖的範圍內。
這部份內容主要是初始化一些屬性,須要注意的一點就是,這裏使用了AtomicLongFieldUpdater
來進行原子更新,保證寫的安全和讀的高效,固然,仍是cocurrent
包的工具。
// 這裏使用了AtomicLongFieldUpdater來進行原子更新,保證了寫的安全和讀的高效 this.id = DruidDriver.createDataSourceId(); if (this.id > 1) { long delta = (this.id - 1) * 100000; this.connectionIdSeedUpdater.addAndGet(this, delta); this.statementIdSeedUpdater.addAndGet(this, delta); this.resultSetIdSeedUpdater.addAndGet(this, delta); this.transactionIdSeedUpdater.addAndGet(this, delta); } // 設置url if (this.jdbcUrl != null) { this.jdbcUrl = this.jdbcUrl.trim(); // 針對druid自定義的一種url格式,進行解析 // jdbc:wrap-jdbc:開頭,可設置driver、name、jmx等 initFromWrapDriverUrl(); } // 根據url前綴,肯定dbType if (this.dbType == null || this.dbType.length() == 0) { this.dbType = JdbcUtils.getDbType(jdbcUrl, null); } // cacheServerConfiguration,暫時不知道這個參數幹嗎用的 if (JdbcConstants.MYSQL.equals(this.dbType) || JdbcConstants.MARIADB.equals(this.dbType) || JdbcConstants.ALIYUN_ADS.equals(this.dbType)) { boolean cacheServerConfigurationSet = false; if (this.connectProperties.containsKey("cacheServerConfiguration")) { cacheServerConfigurationSet = true; } else if (this.jdbcUrl.indexOf("cacheServerConfiguration") != -1) { cacheServerConfigurationSet = true; } if (cacheServerConfigurationSet) { this.connectProperties.put("cacheServerConfiguration", "true"); } } // 設置驅動類 if (this.driverClass != null) { this.driverClass = driverClass.trim(); } // 若是咱們沒有配置driverClass if (this.driver == null) { // 根據url識別對應的driverClass if (this.driverClass == null || this.driverClass.isEmpty()) { this.driverClass = JdbcUtils.getDriverClassName(this.jdbcUrl); } // MockDriver的狀況,這裏不討論 if (MockDriver.class.getName().equals(driverClass)) { driver = MockDriver.instance; } else { if (jdbcUrl == null && (driverClass == null || driverClass.length() == 0)) { throw new SQLException("url not set"); } // 建立Driver實例,注意,這個過程不須要依賴DriverManager driver = JdbcUtils.createDriver(driverClassLoader, driverClass); } } else { if (this.driverClass == null) { this.driverClass = driver.getClass().getName(); } } // 用於存放全部鏈接對象 connections = new DruidConnectionHolder[maxActive]; // 用於存放須要丟棄的鏈接對象 evictConnections = new DruidConnectionHolder[maxActive]; // 用於存放須要keepAlive的鏈接對象 keepAliveConnections = new DruidConnectionHolder[maxActive];
看到下面的代碼會發現,咱們還能夠經過SPI機制來配置過濾器。
使用SPI配置過濾器時須要注意,對應的類須要加上@AutoLoad
註解,另外還須要配置load.spifilter.skip=false
,SPI相關內容可參考個人另外一篇博客:使用SPI解耦你的實現類。
在這個方法裏,主要就是初始化過濾器的一些屬性而已。過濾器的部分,本文不會涉及到太多。
// 初始化filters for (Filter filter : filters) { filter.init(this); } // 採用SPI機制加載過濾器,這部分過濾器除了放入filters,還會放入autoFilters initFromSPIServiceLoader();
這裏只是簡單的校驗,不涉及太多複雜的邏輯。
// 校驗maxActive、minIdle、initialSize、timeBetweenLogStatsMillis、useGlobalDataSourceStat、maxEvictableIdleTimeMillis、minEvictableIdleTimeMillis等配置是否合法 // ······· // 針對oracle和DB2,須要校驗validationQuery initCheck(); // 當開啓了testOnBorrow/testOnReturn/testWhileIdle,判斷是否設置了validationQuery,沒有的話會打印錯誤信息 validationQueryCheck();
這裏重點關注ExceptionSorter
和ValidConnectionChecker
這兩個類,這裏會根據數據庫類型進行選擇。其中,ValidConnectionChecker
用於對鏈接進行檢測。
// 根據driverClassName初始化ExceptionSorter initExceptionSorter(); // 根據driverClassName初始化ValidConnectionChecker initValidConnectionChecker(); // 初始化dataSourceStat // 若是設置了isUseGlobalDataSourceStat爲true,則支持公用監控數據 if (isUseGlobalDataSourceStat()) { dataSourceStat = JdbcDataSourceStat.getGlobal(); if (dataSourceStat == null) { dataSourceStat = new JdbcDataSourceStat("Global", "Global", this.dbType); JdbcDataSourceStat.setGlobal(dataSourceStat); } if (dataSourceStat.getDbType() == null) { dataSourceStat.setDbType(this.dbType); } } else { dataSourceStat = new JdbcDataSourceStat(this.name, this.jdbcUrl, this.dbType, this.connectProperties); } dataSourceStat.setResetStatEnable(this.resetStatEnable);
這裏有兩種方式建立鏈接,一種是異步,一種是同步。可是,根據咱們的使用例子,createScheduler
爲null,因此採用的是同步的方式。
注意,後面的全部代碼也是基於createScheduler
爲null來分析的。
// 建立初始鏈接數 // 異步建立,createScheduler爲null,不進入 if (createScheduler != null && asyncInit) { for (int i = 0; i < initialSize; ++i) { submitCreateTask(true); } // 同步建立 } else if (!asyncInit) { // 建立鏈接的過程後面再講 while (poolingCount < initialSize) { PhysicalConnectionInfo pyConnectInfo = createPhysicalConnection(); DruidConnectionHolder holder = new DruidConnectionHolder(this, pyConnectInfo); connections[poolingCount++] = holder; } if (poolingCount > 0) { poolingPeak = poolingCount; poolingPeakTime = System.currentTimeMillis(); } }
這裏會啓動三個線程。
// 啓動監控數據記錄線程 createAndLogThread(); // 啓動鏈接建立線程 createAndStartCreatorThread(); // 啓動鏈接檢測線程 createAndStartDestroyThread();
這裏使用了CountDownLatch
,保證當createConnectionThread
和destroyConnectionThread
開始run時再繼續執行。
private final CountDownLatch initedLatch = new CountDownLatch(2); // 線程進入等待,等待CreatorThread和DestroyThread執行 initedLatch.await();
咱們進入到DruidDataSource.CreateConnectionThread.run()
,能夠看到,一執行run方法就會調用countDown
。destroyConnectionThread
也是同樣,這裏就不放進來了。
public class CreateConnectionThread extends Thread { public void run() { initedLatch.countDown(); // do something } }
接下來是註冊MBean
,會去註冊DruidDataSourceStatManager
和DruidDataSource
,啓動咱們的程度,經過jconsole就能夠看到這兩個MBean
。JMX相關內容這裏就很少擴展了,感興趣的話可參考個人另外一篇博客: 如何使用JMX來管理程序?
// 註冊MBean,用於支持JMX registerMbean();
前面已經講過,當咱們調用empty.signal()
,會去喚醒處於empty.await()
狀態的CreateConnectionThread
。CreateConnectionThread
這個線只有在須要建立鏈接時才運行,不然會一直等待,後面會講到。
protected Condition empty; if (keepAlive) { // 這裏會去調用empty.signal(),會去喚醒處於empty.await()狀態的CreateConnectionThread this.emptySignal(); }
用戶調用DruidDataSource.getConnection
,拿到的對象時DruidPooledConnection
,裏面封裝了DruidConnectionHolder
,而這個對象包含了原生的鏈接對象和咱們一開始建立的數據源對象。
鏈接對象的獲取過程能夠歸納爲如下步驟:
createConnectionThread
發送signal建立新鏈接,此時會進入等待;testOnBorrow
,進行testOnBorrow
檢測,不然,若是設置了testWhileIdle
,進行testWhileIdle
檢測;removeAbandoned
,則會將鏈接對象放入activeConnections
;defaultAutoCommit
,並返回;filterChain
。初始化數據源的前面已經講過了,這裏就直接從第二步開始。
進入DruidDataSource.getConnectionInternal
方法。除了獲取鏈接對象,其餘的大部分是校驗和計數的內容。
private DruidPooledConnection getConnectionInternal(long maxWait) throws SQLException { // 校驗數據源是否可用 // ······ final long nanos = TimeUnit.MILLISECONDS.toNanos(maxWait); final int maxWaitThreadCount = this.maxWaitThreadCount; DruidConnectionHolder holder; // 加鎖 try { lock.lockInterruptibly(); } catch(InterruptedException e) { connectErrorCountUpdater.incrementAndGet(this); throw new SQLException("interrupt", e); } try { // 判斷當前等待線程是否超過maxWaitThreadCount if(maxWaitThreadCount > 0 && notEmptyWaitThreadCount >= maxWaitThreadCount) { connectErrorCountUpdater.incrementAndGet(this); throw new SQLException("maxWaitThreadCount " + maxWaitThreadCount + ", current wait Thread count " + lock.getQueueLength()); } // 根據是否設置maxWait選擇不一樣的獲取方式,後面選擇未設置maxWait的方法來分析 if(maxWait > 0) { holder = pollLast(nanos); } else { holder = takeLast(); } // activeCount(全部活躍鏈接數量)+1,並設置峯值 if(holder != null) { activeCount++; if(activeCount > activePeak) { activePeak = activeCount; activePeakTime = System.currentTimeMillis(); } } } catch(InterruptedException e) { connectErrorCountUpdater.incrementAndGet(this); throw new SQLException(e.getMessage(), e); } catch(SQLException e) { connectErrorCountUpdater.incrementAndGet(this); throw e; } finally { // 解鎖 lock.unlock(); } // 當拿到的對象爲空時,拋出異常 if (holder == null) { // ······ } // 鏈接對象的useCount(使用次數)+1 holder.incrementUseCount(); // 包裝下後返回 DruidPooledConnection poolalbeConnection = new DruidPooledConnection(holder); return poolalbeConnection; }
下面再看下DruidDataSource.takeLast()
方法(即沒有配置maxWait時調用的方法)。該方法中,當沒有空閒鏈接對象時,會嘗試建立鏈接,此時該線程進入等待(notEmpty.await()
),只有鏈接對象建立完成或池中回收了鏈接對象(notEmpty.signal()
),該線程纔會繼續執行。
DruidConnectionHolder takeLast() throws InterruptedException, SQLException { try { // 若是當前池中無空閒鏈接,由於沒有設置maxWait,會一直循環地去獲取 while (poolingCount == 0) { // 向CreateConnectionThread發送signal,通知建立鏈接對象 emptySignal(); // send signal to CreateThread create connection // 快速失敗 if (failFast && isFailContinuous()) { throw new DataSourceNotAvailableException(createError); } // notEmptyWaitThreadCount(等待鏈接對象的線程數)+1,並設置峯值 notEmptyWaitThreadCount++; if (notEmptyWaitThreadCount > notEmptyWaitThreadPeak) { notEmptyWaitThreadPeak = notEmptyWaitThreadCount; } try { // 等待鏈接對象建立完成或池中回收了鏈接對象 notEmpty.await(); // signal by recycle or creator } finally { // notEmptyWaitThreadCount(等待鏈接對象的線程數)-1 notEmptyWaitThreadCount--; } // notEmptyWaitCount(等待次數)+1 notEmptyWaitCount++; } } catch (InterruptedException ie) { // TODO 這裏是在notEmpty.await()時拋出的,不知爲何要notEmpty.signal()? notEmpty.signal(); // propagate to non-interrupted thread // notEmptySignalCount+1 notEmptySignalCount++; throw ie; } // poolingCount(空閒鏈接)-1 decrementPoolingCount(); // 獲取數組中最後一個鏈接對象 DruidConnectionHolder last = connections[poolingCount]; connections[poolingCount] = null; return last; }
前面已經講到,建立鏈接是採用異步方式,進入到DruidDataSource.CreateConnectionThread.run()
。當不須要建立鏈接時,該線程進入empty.await()
狀態,此時須要用戶線程調用empty.signal()
來喚醒。
public void run() { // 用於喚醒初始化數據源的線程 initedLatch.countDown(); long lastDiscardCount = 0; // 注意,這裏是死循環,當須要建立鏈接對象時,這個線程會受到signal,不然會一直await for (;;) { // 加鎖 try { lock.lockInterruptibly(); } catch (InterruptedException e2) { break; } // 丟棄數量discardCount long discardCount = DruidDataSource.this.discardCount; boolean discardChanged = discardCount - lastDiscardCount > 0; lastDiscardCount = discardCount; try { // 這個變量表明瞭是否有必要新增鏈接,true表明不必 boolean emptyWait = true; if (createError != null && poolingCount == 0 && !discardChanged) { emptyWait = false; } if (emptyWait && asyncInit && createCount < initialSize) { emptyWait = false; } if (emptyWait) { // 必須存在線程等待,才建立鏈接 if (poolingCount >= notEmptyWaitThreadCount // && (!(keepAlive && activeCount + poolingCount < minIdle)) && !isFailContinuous() ) { // 等待signal,前面已經講到,當某線程須要建立鏈接時,會發送signal給它 empty.await(); } // 防止建立超過maxActive數量的鏈接 if (activeCount + poolingCount >= maxActive) { empty.await(); continue; } } } catch (InterruptedException e) { lastCreateError = e; lastErrorTimeMillis = System.currentTimeMillis(); break; } finally { // 解鎖 lock.unlock(); } PhysicalConnectionInfo connection = null; try { // 建立原生的鏈接對象,幷包裝 connection = createPhysicalConnection(); } catch (SQLException e) { //出現SQLException會繼續往下走 //······ } catch (RuntimeException e) { // 出現RuntimeException則從新進入循環體 LOG.error("create connection RuntimeException", e); setFailContinuous(true); continue; } catch (Error e) { LOG.error("create connection Error", e); setFailContinuous(true); break; } // 若是爲空,從新進入循環體 if (connection == null) { continue; } // 將鏈接對象包裝爲DruidConnectionHolder,並放入connections數組中 // 注意,該方法會去調用notEmpty.signal(),即會去喚醒正在等待獲取鏈接的線程 boolean result = put(connection); } }
進入DruidDataSource.getConnectionDirect(long)
。該方法會使用到validConnectionChecker
來校驗鏈接的有效性。
// 若是開啓了testOnBorrow if (testOnBorrow) { // 這裏會去調用validConnectionChecker的isValidConnection方法來校驗,validConnectionChecker不存在的話,則以普通JDBC方式校驗 boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn); if (!validate) { if (LOG.isDebugEnabled()) { LOG.debug("skip not validate connection."); } Connection realConnection = poolableConnection.conn; // 丟棄鏈接,丟棄完會發送signal給CreateConnectionThread來建立鏈接 discardConnection(realConnection); continue; } } else { Connection realConnection = poolableConnection.conn; if (poolableConnection.conn.isClosed()) { discardConnection(null); // 傳入null,避免重複關閉 continue; } if (testWhileIdle) { final DruidConnectionHolder holder = poolableConnection.holder; // 當前時間 long currentTimeMillis = System.currentTimeMillis(); // 最後活躍時間 long lastActiveTimeMillis = holder.lastActiveTimeMillis; long lastKeepTimeMillis = holder.lastKeepTimeMillis; if (lastKeepTimeMillis > lastActiveTimeMillis) { lastActiveTimeMillis = lastKeepTimeMillis; } // 計算鏈接對象空閒時長 long idleMillis = currentTimeMillis - lastActiveTimeMillis; long timeBetweenEvictionRunsMillis = this.timeBetweenEvictionRunsMillis; // 空閒檢測週期 if (timeBetweenEvictionRunsMillis <= 0) { timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS; } // 當前鏈接空閒時長大於空間檢測週期時,進入檢測 if (idleMillis >= timeBetweenEvictionRunsMillis || idleMillis < 0 // unexcepted branch ) { // 接下來的邏輯和前面testOnBorrow同樣的 boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn); if (!validate) { if (LOG.isDebugEnabled()) { LOG.debug("skip not validate connection."); } discardConnection(realConnection); continue; } } } }
進入DruidDataSource.getConnectionDirect(long)
,這裏不會進行檢測,只是將鏈接對象放入activeConnections
,具體泄露鏈接的檢測工做是在DestroyConnectionThread
線程中進行。
if (removeAbandoned) { StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); poolableConnection.connectStackTrace = stackTrace; // 記錄鏈接借出時間 poolableConnection.setConnectedTimeNano(); poolableConnection.traceEnable = true; activeConnectionLock.lock(); try { // 放入activeConnections activeConnections.put(poolableConnection, PRESENT); } finally { activeConnectionLock.unlock(); } }
DestroyConnectionThread
線程會根據咱們設置的timeBetweenEvictionRunsMillis
來進行檢驗,具體的校驗會去運行DestroyTask
(DruidDataSource
的內部類),這裏看下DestroyTask
的run
方法。
public void run() { // 檢測空閒鏈接的phyTimeoutMillis、idleMillis是否超過指定要求 shrink(true, keepAlive); // 這裏會去調用DruidDataSource.removeAbandoned()進行檢測 if (isRemoveAbandoned()) { removeAbandoned(); } }
進入DruidDataSource.removeAbandoned()
,當鏈接對象使用時間超過removeAbandonedTimeoutMillis
,則會被丟棄掉。
public int removeAbandoned() { int removeCount = 0; long currrentNanos = System.nanoTime(); List<DruidPooledConnection> abandonedList = new ArrayList<DruidPooledConnection>(); // 加鎖 activeConnectionLock.lock(); try { Iterator<DruidPooledConnection> iter = activeConnections.keySet().iterator(); // 遍歷借出的鏈接 for (; iter.hasNext();) { DruidPooledConnection pooledConnection = iter.next(); if (pooledConnection.isRunning()) { continue; } // 計算鏈接對象使用時間 long timeMillis = (currrentNanos - pooledConnection.getConnectedTimeNano()) / (1000 * 1000); // 若是超過設置的丟棄超時時間,則加入abandonedList if (timeMillis >= removeAbandonedTimeoutMillis) { iter.remove(); pooledConnection.setTraceEnable(false); abandonedList.add(pooledConnection); } } } finally { // 解鎖 activeConnectionLock.unlock(); } // 遍歷須要丟棄的鏈接對象 if (abandonedList.size() > 0) { for (DruidPooledConnection pooledConnection : abandonedList) { final ReentrantLock lock = pooledConnection.lock; // 加鎖 lock.lock(); try { // 若是該鏈接已經失效,則繼續循環 if (pooledConnection.isDisable()) { continue; } } finally { // 解鎖 lock.unlock(); } // 關閉鏈接 JdbcUtils.close(pooledConnection); pooledConnection.abandond(); removeAbandonedCount++; removeCount++; } } return removeCount; }
進入DruidDataSource.getConnection
。
public DruidPooledConnection getConnection(long maxWaitMillis) throws SQLException { // 初始化數據源(若是還沒初始化) init(); // 若是設置了過濾器,會先執行每一個過濾器的方法 if (filters.size() > 0) { FilterChainImpl filterChain = new FilterChainImpl(this); // 這裏會去遞歸調用過濾器的方法 return filterChain.dataSource_connect(this, maxWaitMillis); } else { // 若是沒有設置過濾器,直接去獲取鏈接對象 return getConnectionDirect(maxWaitMillis); } }
進入到FilterChainImpl.dataSource_connect
。
public DruidPooledConnection dataSource_connect(DruidDataSource dataSource, long maxWaitMillis) throws SQLException { // 當指針小於過濾器數量 // pos表示過濾器的索引 if (this.pos < filterSize) { // 拿到第一個過濾器並調用它的dataSource_getConnection方法 DruidPooledConnection conn = getFilters().get(pos++).dataSource_getConnection(this, dataSource, maxWaitMillis); return conn; } // 當訪問到最後一個過濾器時,纔會去建立鏈接 return dataSource.getConnectionDirect(maxWaitMillis); }
這裏以StatFilter.dataSource_getConnection
爲例。
public DruidPooledConnection dataSource_getConnection(FilterChain chain, DruidDataSource dataSource, long maxWaitMillis) throws SQLException { // 這裏又回到FilterChainImpl.dataSource_connect方法 DruidPooledConnection conn = chain.dataSource_connect(dataSource, maxWaitMillis); if (conn != null) { conn.setConnectedTimeNano(); StatFilterContext.getInstance().pool_connection_open(); } return conn; }
以上,druid的源碼基本已經分析完,其餘部份內容有空再作補充。
本文爲原創文章,轉載請附上原文出處連接:http://www.javashuo.com/article/p-aueunzek-bt.html
原文出處:https://www.cnblogs.com/ZhangZiSheng001/p/12175893.html