原始碼詳解系列(六) ------ 全面講解druid的使用和原始碼
簡介
druid
是用於建立和管理連線,利用“池”的方式複用連線減少資源開銷,和其他資料來源一樣,也具有連線數控制、連線可靠性測試、連線洩露控制、快取語句等功能,另外,druid
還擴充套件了監控統計、防禦SQL注入等功能。
本文將包含以下內容(因為篇幅較長,可根據需要選擇閱讀):
druid
的使用方法(入門案例、JDNI
使用、監控統計、防禦SQL注入)druid
的配置引數詳解druid
主要原始碼分析
使用例子-入門
需求
使用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() {
// 建立sql
String sql = "insert into demo_user values(null,?,?,?,?,?)";
Connection connection = null;
PreparedStatement statement = null;
try {
// 獲得連線
connection = JDBCUtil.getConnection();
// 開啟事務設定非自動提交
JDBCUtil.startTrasaction();
// 獲得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();
// 提交事務
JDBCUtil.commit();
} catch(Exception e) {
JDBCUtil.rollback();
log.error("儲存使用者失敗", e);
} finally {
// 釋放資源
JDBCUtil.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