簡介
druid
是用於創建和管理連接,利用“池”的方式復用連接減少資源開銷,和其他數據源一樣,也具有連接數控制、連接可靠性測試、連接泄露控制、緩存語句等功能,另外,druid
還擴展了監控統計、防御SQL注入等功能。
本文將包含以下內容(因為篇幅較長,可根據需要選擇閱讀):
druid
的使用方法(入門案例、JDNI
使用、監控統計、防御SQL注入)druid
的配置參數詳解druid
主要源碼分析
其他連接池的內容也可以參考我的其他博客:
源碼詳解系列(四) ------ DBCP2的使用和分析(包括JNDI和JTA支持)
源碼詳解系列(五) ------ C3P0的使用和分析(包括JNDI)
使用例子-入門
需求
使用druid
連接池獲取連接對象,對用戶數據進行簡單的增刪改查(sql
腳本項目中已提供)。
工程環境
JDK
:1.8.0_231
maven
:3.6.1
IDE
:eclipse 4.12
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>
編寫druid.properties
配置文件路徑在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>
編寫context.xml
在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.xml
在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>
編寫jsp
因為需要在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
使用例子-開啟監控統計
在以上例子基礎上修改。
配置StatFilter
打開監控統計功能
druid的監控統計功能是通過filter-chain
擴展實現,如果你要打開監控統計功能,配置StatFilter
,如下:
filters=stat
stat是com.alibaba.druid.filter.stat.StatFilter
的別名,別名映射配置信息保存在druid-xxx.jar!/META-INF/druid-filter.properties
。
SQL合並配置
當你程序中存在沒有參數化的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
的缺省值。
慢SQL記錄
StatFilter
屬性slowSqlMillis
用來配置SQL慢的標准,執行時間超過slowSqlMillis
的就是慢。slowSqlMillis
的缺省值為3000,也就是3秒。
connectionProperties=druid.stat.logSlowSql=true;druid.stat.slowSqlMillis=5000
在上面的配置中,slowSqlMillis被修改為5秒,並且通過日志輸出執行慢的SQL。
合並多個DruidDataSource的監控數據
缺省多個DruidDataSource
的監控數據是各自獨立的,在druid-0.2.17版本之后,支持配置公用監控數據,配置參數為useGlobalDataSourceStat
。例如:
connectionProperties=druid.useGlobalDataSourceStat=true
配置StatViewServlet
druid內置提供了一個StatViewServlet
用於展示Druid的統計信息。
這個StatViewServlet
的用途包括:
- 提供監控信息展示的html頁面
- 提供監控信息的JSON API
注意:使用StatViewServlet
,建議使用druid 0.2.6以上版本。
配置web.xml
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>
配置allow和deny
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
沒有配置或者為空,則允許所有訪問
配置resetEnable
在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
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統計和監控,等等:
使用例子-防御SQL注入
開啟WallFilter
WallFilter
用於對SQL進行攔截,通過以下配置開啟:
#過濾器
filters=wall,stat
注意,這種配置攔截檢測的時間不在StatFilter
統計的SQL執行時間內。 如果希望StatFilter
統計的SQL執行時間內,則使用如下配置
#過濾器
filters=stat,wall
WallConfig詳細說明
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注入類型的風險 |
使用例子-日志記錄JDBC執行的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.properties配置
如果你使用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
輸出可執行的SQL
參數配置方式
connectionProperties=druid.log.stmt.executableSql=true
配置文件詳解
配置druid的參數的n種方式
使用druid,同一個參數,我們可以采用多種方式進行配置,舉個例子:maxActive
(最大連接池參數)的配置:
方式一(系統屬性)
系統屬性一般在啟動參數中設置。通過方式一來配置連接池參數的還是比較少見。
-Ddruid.maxActive=8
方式二(properties)
這是最常見的一種。
maxActive=8
方式三(properties加前綴)
相比第二種方式,這里只是加了.druid
前綴。
druid.maxActive=8
方式四(properties的connectionProperties)
connectionProperties
可以用於配置多個屬性,不同屬性使用";"隔開。
connectionProperties=druid.maxActive=8
方式五(connectProperties)
connectProperties
可以在方式一、方式三和方式四中存在,具體配置如下:
# 方式一
-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的參數配置還存在另一個問題,先看下這個表格(這里包含了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有以下特點:
- 更多地引入了JDK的特性,特別是concurrent包的工具。例如,
CountDownLatch
、ReentrantLock
、AtomicLongFieldUpdater
、Condition
等,也就是說,在分析druid源碼之前,最好先學習下這些技術; - 在類的設計上一切從簡。例如,DBCP和C3P0都有一個池的類,而druid並沒有,只用了一個簡單的數組,且druid的核心邏輯幾乎都堆積在
DruidDataSource
里面。另外,在對類或接口的抽象上,個人感覺,druid不是很“面向對象”,有的接口或類的方法很難統一成某種對象的行為,所以,本文不會去關注類的設計,更多地將分析一些重要功能的實現。
注意:考慮篇幅和可讀性,以下代碼經過刪減,僅保留所需部分。
配置參數的加載
前面已經講過,druid為我們提供了“無數”種方式來配置參數,這里我再補充下不同配置方式的加載順序(當然,只會涉及到四種方式)。
當我們使用調用DruidDataSourceFactory.createDataSource(Properties)
時,會加載配置來給對應的屬性賦值,另外,這個過程還會根據配置去創建對應的過濾器。不同配置方式加載時機不同,后者會覆蓋已存在的相同參數,如圖所示。

數據源的初始化
了解下DruidDataSource這個類
這里先來介紹下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
,用於支持JMX - 如果設置了
keepAlive
,通知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、JdbcDataSourceStat
這里重點關注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);
創建initialSize數量的連接
這里有兩種方式創建連接,一種是異步,一種是同步。但是,根據我們的使用例子,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();
}
}
創建logStatsThread、createConnectionThread和destroyConnectionThread
這里會啟動三個線程。
// 啟動監控數據記錄線程
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
接下來是注冊MBean
,會去注冊DruidDataSourceStatManager
和DruidDataSource
,啟動我們的程度,通過jconsole就可以看到這兩個MBean
。JMX相關內容這里就不多擴展了,感興趣的話可參考我的另一篇博客: 如何使用JMX來管理程序?
// 注冊MBean,用於支持JMX
registerMbean();
通知createConnectionThread創建連接對象
前面已經講過,當我們調用empty.signal()
,會去喚醒處於empty.await()
狀態的CreateConnectionThread
。CreateConnectionThread
這個線只有在需要創建連接時才運行,否則會一直等待,后面會講到。
protected Condition empty;
if (keepAlive) {
// 這里會去調用empty.signal(),會去喚醒處於empty.await()狀態的CreateConnectionThread
this.emptySignal();
}
連接對象的獲取
了解下DruidPooledConnection這個類
用戶調用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);
}
}
testOnBorrow或testWhileIdle
進入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;
}
}
}
}
removeAbandoned
進入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;
}
執行filterChain
進入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的源碼基本已經分析完,其他部分內容有空再做補充。
參考資料
本文為原創文章,轉載請附上原文出處鏈接:https://www.cnblogs.com/ZhangZiSheng001/p/12175893.html