源碼詳解系列(六) ------ 全面講解druid的使用和源碼

簡介

druid是用於建立和管理鏈接,利用「池」的方式複用鏈接減小資源開銷,和其餘數據源同樣,也具備鏈接數控制、鏈接可靠性測試、鏈接泄露控制、緩存語句等功能,另外,druid還擴展了監控統計、防護SQL注入等功能。css

本文將包含如下內容(由於篇幅較長,可根據須要選擇閱讀):html

  1. druid的使用方法(入門案例、JDNI使用、監控統計、防護SQL注入)
  2. druid的配置參數詳解
  3. 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

主要步驟

  1. 編寫druid.properties,設置數據庫鏈接參數和鏈接池基本參數等

  2. 經過DruidDataSourceFactory加載druid.properties文件,並建立DruidDataSource對象

  3. 經過DruidDataSource對象得到Connection對象

  4. 使用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,並不會影響下面例子的理解,其實能夠理解爲像springbean配置和獲取。

引入依賴

本文在入門例子的基礎上增長如下依賴,由於是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節點都是咱們配置的對象,相似於springbean節點。其中jdbc/druid-test能夠當作是這個beanid

注意,這裏獲取的數據源對象是單例的,若是但願多例,能夠設置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&amp;characterEncoding=utf8&amp;serverTimezone=GMT%2B8&amp;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 = ?

能夠配置StatFiltermergeSql屬性來解決:

#用於設置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

配置監控頁面訪問密碼

須要配置ServletloginUsername 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展現出來的監控信息比較敏感,是系統運行的內部狀況,若是你須要作訪問控制,能夠配置allowdeny這兩個參數。好比:

<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>

判斷規則:

  1. deny優先於allow,若是在deny列表中,就算在allow列表中,也會被拒絕。
  2. 若是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統計和監控,等等:

druid監控統計頁面

使用例子-防護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內置提供了四種LogFilterLog4jFilterLog4j2FilterCommonsLogFilterSlf4jLogFilter),用於輸出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

他們的別名分別是log4jlog4j2slf4jcommonloggingcommonLogging。其中commonloggingcommonLogging只是大小寫不一樣。

配置輸出日誌

缺省輸入的日誌信息全面,可是內容比較多,有時候咱們須要定製化配置日誌輸出。

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能夠在方式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的參數配置還存在另外一個問題,先看下這個表格(這裏包含了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有如下特色:

  1. 更多地引入了JDK的特性,特別是concurrent包的工具。例如,CountDownLatchReentrantLockAtomicLongFieldUpdaterCondition等,也就是說,在分析druid源碼以前,最好先學習下這些技術;
  2. 在類的設計上一切從簡。例如,DBCP和C3P0都有一個池的類,而druid並無,只用了一個簡單的數組,且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這個類

這裏先來介紹下DruidDataSource這個類:

DruidDataSource的UML圖

圖中我只列出了幾個重要的屬性,這幾個屬性沒有理解好,後面的源碼很難看得進去。

類名 描述
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()這個方法,能夠歸納爲如下步驟:

  1. 加鎖
  2. 初始化initStackTraceidxxIdSeeddbTypdriverdataSourceStatconnectionsevictConnectionskeepAliveConnections等屬性
  3. 初始化過濾器
  4. 校驗maxActiveminIdleinitialSizetimeBetweenLogStatsMillisuseGlobalDataSourceStatmaxEvictableIdleTimeMillisminEvictableIdleTimeMillisvalidationQuery等配置是否合法
  5. 初始化ExceptionSorterValidConnectionCheckerJdbcDataSourceStat
  6. 建立initialSize數量的鏈接
  7. 建立logStatsThreadcreateConnectionThreaddestroyConnectionThread
  8. 等待createConnectionThreaddestroyConnectionThread線程run後再繼續執行
  9. 註冊MBean,用於支持JMX
  10. 若是設置了keepAlive,通知createConnectionThread建立鏈接對象
  11. 解鎖

這個方法差很少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

這裏重點關注ExceptionSorterValidConnectionChecker這兩個類,這裏會根據數據庫類型進行選擇。其中,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,保證當createConnectionThreaddestroyConnectionThread開始run時再繼續執行。

private final CountDownLatch initedLatch = new CountDownLatch(2);
        // 線程進入等待,等待CreatorThread和DestroyThread執行
        initedLatch.await();

咱們進入到DruidDataSource.CreateConnectionThread.run(),能夠看到,一執行run方法就會調用countDowndestroyConnectionThread也是同樣,這裏就不放進來了。

public class CreateConnectionThread extends Thread {

        public void run() {
            initedLatch.countDown();
            // do something
        }
    }

註冊MBean

接下來是註冊MBean,會去註冊DruidDataSourceStatManagerDruidDataSource,啓動咱們的程度,經過jconsole就能夠看到這兩個MBean。JMX相關內容這裏就很少擴展了,感興趣的話可參考個人另外一篇博客: 如何使用JMX來管理程序?

// 註冊MBean,用於支持JMX
        registerMbean();

通知createConnectionThread建立鏈接對象

前面已經講過,當咱們調用empty.signal(),會去喚醒處於empty.await()狀態的CreateConnectionThreadCreateConnectionThread這個線只有在須要建立鏈接時才運行,不然會一直等待,後面會講到。

protected Condition empty;
        if (keepAlive) {
            // 這裏會去調用empty.signal(),會去喚醒處於empty.await()狀態的CreateConnectionThread
            this.emptySignal();
        }

鏈接對象的獲取

瞭解下DruidPooledConnection這個類

用戶調用DruidDataSource.getConnection,拿到的對象時DruidPooledConnection,裏面封裝了DruidConnectionHolder,而這個對象包含了原生的鏈接對象和咱們一開始建立的數據源對象。

DruidPooledConnection的UML圖

歸納下獲取鏈接的過程

鏈接對象的獲取過程能夠歸納爲如下步驟:

  1. 初始化數據源(若是還沒初始化);
  2. 得到鏈接對象,若是無可用鏈接,向createConnectionThread發送signal建立新鏈接,此時會進入等待;
  3. 若是設置了testOnBorrow,進行testOnBorrow檢測,不然,若是設置了testWhileIdle,進行testWhileIdle檢測;
  4. 若是設置了removeAbandoned,則會將鏈接對象放入activeConnections
  5. 設置defaultAutoCommit,並返回;
  6. 執行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來進行檢驗,具體的校驗會去運行DestroyTaskDruidDataSource的內部類),這裏看下DestroyTaskrun方法。

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的源碼基本已經分析完,其餘部份內容有空再作補充。

參考資料

druid的github倉庫資料

相關源碼請移步:https://github.com/ZhangZiSheng001/druid-demo

本文爲原創文章,轉載請附上原文出處連接:http://www.javashuo.com/article/p-aueunzek-bt.html

原文出處:https://www.cnblogs.com/ZhangZiSheng001/p/12175893.html

相關文章
相關標籤/搜索