1. 程式人生 > >原始碼詳解系列(六) ------ 全面講解druid的使用和原始碼

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

簡介

druid是用於建立和管理連線,利用“池”的方式複用連線減少資源開銷,和其他資料來源一樣,也具有連線數控制、連線可靠性測試、連線洩露控制、快取語句等功能,另外,druid還擴充套件了監控統計、防禦SQL注入等功能。

本文將包含以下內容(因為篇幅較長,可根據需要選擇閱讀):

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

主要步驟

  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() {
        // 建立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,並不會影響下面例子的理解,其實可以理解為像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 = ?

可以配置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>

判斷規則:

  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統計和監控,等等:

使用例子-防禦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有以下特點:

  1. 更多地引入了JDK的特性,特別是concurrent包的工具。例如,CountDownLatch、ReentrantLock、AtomicLongFieldUpdater、Condition等,也就是說,在分析druid原始碼之前,最好先學習下這些技術;
  2. 在類的設計上一切從簡。例如,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()這個方法,可以概括為以下步驟:

  1. 加鎖
  2. 初始化initStackTrace、id、xxIdSeed、dbTyp、driver、dataSourceStat、connections、evictConnections、keepAliveConnections等屬性
  3. 初始化過濾器
  4. 校驗maxActive、minIdle、initialSize、timeBetweenLogStatsMillis、useGlobalDataSourceStat、maxEvictableIdleTimeMillis、minEvictableIdleTimeMillis、validationQuery等配置是否合法
  5. 初始化ExceptionSorter、ValidConnectionChecker、JdbcDataSourceStat
  6. 建立initialSize數量的連線
  7. 建立logStatsThread、createConnectionThread和destroyConnectionThread
  8. 等待createConnectionThread和destroyConnectionThread執行緒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

這裡重點關注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