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

簡介

HikariCP 是用於建立和管理鏈接,利用「池」的方式複用鏈接減小資源開銷,和其餘數據源同樣,也具備鏈接數控制、鏈接可靠性測試、鏈接泄露控制、緩存語句等功能,另外,和 druid 同樣,HikariCP 也支持監控功能。php

HikariCP 是目前最快的鏈接池,就連風靡一時的 BoneCP 也中止維護,主動讓位給它,SpringBoot 也把它設置爲默認鏈接池。html

<img src="https://img2018.cnblogs.com/blog/1731892/202002/1731892-20200219095516084-1441290818.png" style="zoom: 50%;" />java

看過 HikariCP 源碼的同窗就會發現,相比其餘鏈接池,它真的很是輕巧且簡單,有許多值得咱們學習的地方,尤爲性能提高方面,本文也就針對這一方面重點分析。mysql

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

  1. HikariCP 的使用方法(入門案例、JDNI 使用、JMX 使用)
  2. HikariCP 的配置參數詳解
  3. HikariCP 源碼分析

其餘鏈接池的內容也能夠參考個人系列博客:github

源碼詳解系列(四) ------ DBCP2的使用和分析(包括JNDI和JTA支持)web

源碼詳解系列(五) ------ C3P0的使用和分析(包括JNDI)spring

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

使用例子-入門

需求

使用 HikariCP 鏈接池獲取鏈接對象,對用戶數據進行簡單的增刪改查(sql 腳本項目中已提供)。數據庫

工程環境

JDK:1.8.0_231

maven:3.6.1

IDE:Spring Tool Suite 4.3.2.RELEASE

mysql-connector-java:8.0.15

mysql:5.7 .28

Hikari:2.6.1

主要步驟

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

  2. 經過HikariConfig加載 hikari.properties 文件,並建立HikariDataSource對象;

  3. 經過HikariDataSource對象得到Connection對象;

  4. 使用Connection對象對用戶表進行增刪改查。

建立項目

項目類型Maven Project,打包方式war(其實jar也能夠,之因此使用war是爲了測試 JNDI)。

引入依賴

這裏引入日誌包,主要爲了打印配置信息,不引入不會有影響的。

<dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <!-- hikari -->
        <dependency>
            <groupId>com.zaxxer</groupId>
            <artifactId>HikariCP</artifactId>
            <version>2.6.1</version>
        </dependency>
        <!-- mysql驅動 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.15</version>
        </dependency>
        <!-- log -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.28</version>
            <type>jar</type>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.2.3</version>
            <type>jar</type>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
            <type>jar</type>
        </dependency>

編寫hikari.properties

配置文件路徑在resources目錄下,由於是入門例子,這裏僅給出數據庫鏈接參數和鏈接池基本參數,後面會對全部配置參數進行詳細說明。另外,數據庫 sql 腳本也在該目錄下。

#-------------基本屬性--------------------------------
jdbcUrl=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true
username=root
password=root
#JDBC驅動使用的Driver實現類類名
#默認爲空。會根據jdbcUrl來解析
driverClassName=com.mysql.cj.jdbc.Driver

#-------------鏈接池大小相關參數--------------------------------
#最大鏈接池數量
#默認爲10。可經過JMX動態修改
maximumPoolSize=10

#最小空閒鏈接數量
#默認與maximumPoolSize一致。可經過JMX動態修改
minimumIdle=0

獲取鏈接池和獲取鏈接

項目中編寫了JDBCUtil來初始化鏈接池、獲取鏈接、管理事務和釋放資源等,具體參見項目源碼。

路徑:cn.zzs.hikari

HikariConfig config = new HikariConfig("/hikari.properties");
    DataSource dataSource = new HikariDataSource(config);

編寫測試類

這裏以保存用戶爲例,路徑在 test 目錄下的cn.zzs.hikari

@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 獲取HikariDataSource對象,選擇使用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/hikariCP-test能夠當作是這個 bean 的 id。

HikariCP 提供了HikariJNDIFactory來支持 JNDI 。

注意,這裏獲取的數據源對象是單例的,若是但願多例,能夠設置singleton="false"

<?xml version="1.0" encoding="UTF-8"?>
<Context>
  <Resource
      name="jdbc/hikariCP-test"
      factory="com.zaxxer.hikari.HikariJNDIFactory"
      auth="Container"
      type="javax.sql.DataSource"
   
      jdbcUrl="jdbc:mysql://localhost:3306/github_demo?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=GMT%2B8&amp;useSSL=true"
      username="root"
      password="root"
      driverClassName="com.mysql.cj.jdbc.Driver"
      maximumPoolSize="10"
      minimumIdle="0"
      />
</Context>

編寫web.xml

web-app節點下配置資源引用,每一個resource-ref指向了咱們配置好的對象。

<!-- JNDI數據源 -->
    <resource-ref>
        <res-ref-name>jdbc/hikariCP-test</res-ref-name>
        <res-type>javax.sql.DataSource</res-type>
        <res-auth>Container</res-auth>
    </resource-ref>

編寫jsp

由於須要在web環境中使用,若是直接建類寫個main方法測試,會一直報錯的,目前沒找到好的辦法。這裏就簡單地使用jsp來測試吧。

<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/hikari-demo/testJNDI.jsp ,控制檯打印以下內容:

用戶名:zzs001,年齡:18
用戶名:zzs002,年齡:18
用戶名:zzs003,年齡:25
用戶名:zzf001,年齡:26
用戶名:zzf002,年齡:17
用戶名:zzf003,年齡:18

使用例子-經過JMX管理鏈接池

需求

開啓 HikariCP 的 JMX 功能,並使用 jconsole 查看。

修改hikari.properties

在例子一基礎上增長以下配置。這要設置 registerMbeans 爲 true,JMX 功能就會開啓。

#-------------JMX--------------------------------
#是否容許經過JMX掛起和恢復鏈接池
#默認爲false
allowPoolSuspension=false

#是否開啓JMX
#默認false
registerMbeans=true

#數據源名,通常用於JMX。
#默認自動生成
poolName=zzs001

編寫測試類

爲了查看具體效果,這裏讓主線程進入睡眠,避免結束。

public static void main(String[] args) throws InterruptedException {
        new HikariDataSourceTest().findAll();
        Thread.sleep(60 * 60 * 1000);
    }

使用jconsole查看

運行項目,打開 jconsole,選擇咱們的項目後點鏈接,在 MBean 選項卡能夠看到咱們的項目。經過 PoolConfig 能夠動態修改配置(只有部分參數容許修改);經過 Pool 能夠獲取鏈接池的鏈接數(活躍、空閒和全部)、獲取等待鏈接的線程數、掛起和恢復鏈接池、丟棄未使用鏈接等。

<img src="https://img2018.cnblogs.com/blog/1731892/202002/1731892-20200219095603999-720683005.png" alt="hikariCP_jmx" style="zoom:80%;" />

想了解更多 JMX 功能能夠參考個人博客文章: 如何使用JMX來管理程序?

配置文件詳解編寫

相比其餘鏈接池,HikariCP 的配置參數很是簡單,其中有幾個功能須要注意:HikariCP 強制開啓借出測試和空閒測試,不開啓回收測試,可選的只有泄露測試。

數據庫鏈接參數

注意,這裏在url後面拼接了多個參數用於避免亂碼、時區報錯問題。 補充下,若是不想加入時區的參數,能夠在mysql命令窗口執行以下命令:set global time_zone='+8:00'

#-------------基本屬性--------------------------------
jdbcUrl=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true
username=root
password=root
#JDBC驅動使用的Driver實現類類名
#默認爲空。會根據jdbcUrl來解析
driverClassName=com.mysql.cj.jdbc.Driver

鏈接池數據基本參數

這兩個參數都比較經常使用,建議根據具體項目調整。

#-------------鏈接池大小相關參數--------------------------------
#最大鏈接池數量
#默認爲10。可經過JMX動態修改
maximumPoolSize=10

#最小空閒鏈接數量
#默認與maximumPoolSize一致。可經過JMX動態修改
minimumIdle=0

鏈接檢查參數

針對鏈接失效的問題,HikariCP 強制開啓借出測試和空閒測試,不開啓回收測試,可選的只有泄露測試。

#-------------鏈接檢測狀況--------------------------------
#用來檢測鏈接是否有效的sql,要求是一個查詢語句,經常使用select 'x'
#若是驅動支持JDBC4,建議不設置,由於這時默認會調用Connection.isValid()方法來檢測,該方式效率會更高
#默認爲空
connectionTestQuery=select 1 from dual

#檢測鏈接是否有效的超時時間,單位毫秒
#最小容許值250 ms
#默認5000 ms。可經過JMX動態修改
validationTimeout=5000

#鏈接保持空閒而不被驅逐的最小時間。單位毫秒。
#該配置只有再minimumIdle < maximumPoolSize纔會生效,最小容許值爲10000 ms。
#默認值10000*60 = 10分鐘。可經過JMX動態修改
idleTimeout=600000

#鏈接對象容許「泄露」的最大時間。單位毫秒
#最小容許值爲2000 ms。
#默認0,表示不開啓泄露檢測。可經過JMX動態修改
leakDetectionThreshold=0

#鏈接最大存活時間。單位毫秒
#最小容許值30000 ms
#默認30分鐘。可經過JMX動態修改
maxLifetime=1800000

#獲取鏈接時最大等待時間,單位毫秒
#獲取時間超過該配置,將拋出異常。最小容許值250 ms
#默認30000 ms。可經過JMX動態修改
connectionTimeout=300000

#在啓動鏈接池前獲取鏈接的超時時間,單位毫秒
#>0時,會嘗試獲取鏈接。若是獲取時間超過指定時長,不會開啓鏈接池,並拋出異常
#=0時,會嘗試獲取並驗證鏈接。若是獲取成功但驗證失敗則不開啓池,可是若是獲取失敗仍是會開啓池
#<0時,無論是否獲取或校驗成功都會開啓池。
#默認爲1
initializationFailTimeout=1

事務相關參數

建議保留默認就行。

#-------------事務相關的屬性--------------------------------
#當鏈接返回池中時是否設置自動提交
#默認爲true
autoCommit=true

#當鏈接從池中取出時是否設置爲只讀
#默認值false
readOnly=false

#鏈接池建立的鏈接的默認的TransactionIsolation狀態
#可用值爲下列之一:NONE,TRANSACTION_READ_UNCOMMITTED, TRANSACTION_READ_COMMITTED, TRANSACTION_REPEATABLE_READ, TRANSACTION_SERIALIZABLE
#默認值爲空,由驅動決定
transactionIsolation=TRANSACTION_REPEATABLE_READ

#是否在事務中隔離內部查詢。
#autoCommit爲false時才生效
#默認false
isolateInternalQueries=false

JMX參數

建議不開啓 allowPoolSuspension,對性能影響較大,後面源碼分析會解釋緣由。

#-------------JMX--------------------------------

#是否容許經過JMX掛起和恢復鏈接池
#默認爲false
allowPoolSuspension=false

#是否開啓JMX
#默認false
registerMbeans=true

#數據源名,通常用於JMX。
#默認自動生成
poolName=zzs001

其餘

注意,這裏的 dataSourceJndiName 不是前面例子中的 jdbc/hikariCP-test,這個數據源是用來建立原生鏈接對象的,通常用不到。

#-------------其餘--------------------------------
#數據庫目錄
#默認由驅動決定
catalog=github_demo

#由JDBC驅動提供的數據源類名
#不支持XA數據源。若是不設置,默認會採用DriverManager來獲取鏈接對象
#注意,若是設置了driverClassName,則不容許再設置dataSourceClassName,不然會報錯
#默認爲空
#dataSourceClassName=

#JNDI配置的數據源名
#默認爲空
#dataSourceJndiName=

#在每一個鏈接獲取後、放入池前,須要執行的初始化語句
#若是執行失敗,該鏈接會被丟棄
#默認爲空
#connectionInitSql=

#-------------如下參數僅支持經過IOC容器或代碼配置的方式--------------------------------

#TODO
#默認爲空
#metricRegistry

#TODO
#默認爲空
#healthCheckRegistry

#用於Hikari包裝的數據源實例
#默認爲空
#dataSource

#用於建立線程的工廠
#默認爲空
#threadFactory=

#用於執行定時任務的線程池
#默認爲空
#scheduledExecutor=

源碼分析

HikariCP 的源碼輕巧且簡單,讀起來不會太吃力,因此,此次不會從頭至尾地分析代碼邏輯,更多地會分析一些設計巧妙的地方。

在閱讀 HiakriCP 源碼以前,須要掌握:CopyOnWriteArrayListAtomicIntegerSynchronousQueueSemaphoreAtomicIntegerFieldUpdater等工具。

注意:考慮篇幅和可讀性,如下代碼通過刪減,僅保留所需部分 。

HikariCP爲何快?

結合源碼分析以及參考資料,相比 DBCP 和 C3P0 等鏈接池,HikariCP 快主要有如下幾個緣由:

  1. 經過代碼設計和優化大幅減小線程間的鎖競爭。這一點主要經過 ConcurrentBag 來實現,下文會展開。
  2. 引入了更多 JDK 的特性,尤爲是 concurrent 包的工具。DBCP 和 C3P0 出現時間較早,基於早期的 JDK 進行開發,也就很難享受到後面更新帶來的福利;
  3. 使用 javassist 直接修改 class 文件生成動態代理,精簡了不少沒必要要的字節碼,提升代理方法運行速度。相比 JDK 和 cglib 的動態代理,經過 javassist 直接修改 class 文件生成的代理類在運行上會更快一些(這是網上找到的說法,可是目前 JDK 和 cglib 已經通過了屢次優化,在代理類的運行速度上應該不會差一個數量級,我抽空再測試下吧)。HikariCP 涉及 javassist 的代碼在 JavassistProxyFactory 類中,相關內容請自行查閱;
  4. 重視代碼細節對性能的影響。下文到的 fastPathPool 就是一個例子,仔細琢磨 HikariCP 的代碼就會發現許多相似的細節優化,除此以外還有 FastList 等自定義集合類;

接下來,本文將在分析源碼的過程當中對以上幾點展開討論。

HikariCP的架構

在分析具體代碼以前,這裏先介紹下 HikariCP 的總體架構,和 DBCP2 的有點相似(可見 HikariCP 與 DBCP2 性能差別並非因爲架構設計)。

<img src="https://img2018.cnblogs.com/blog/1731892/202002/1731892-20200219095639865-1841717102.png" alt="HikariUML.png" style="zoom: 80%;" />

咱們和 HikariCP 打交道,通常經過如下幾個入口:

  1. 經過 JMX 調用HikariConfigMXBean來動態修改配置(只有部分參數容許修改,在配置詳解裏有註明);

  2. 經過 JMX 調用HikariPoolMXBean來獲取鏈接池的鏈接數(活躍、空閒和全部)、獲取等待鏈接的線程數、掛起和恢復鏈接池、丟棄未使用鏈接等;

  3. 使用HikariConfig加載配置文件,或手動配置HikariConfig的參數,通常它會做爲入參來構造HikariDataSource對象;

  4. 使用HikariDataSource獲取和丟棄鏈接對象,另外,由於繼承了HikariConfig,咱們也能夠經過HikariDataSource來配置參數,但這種方式不支持配置文件。

爲何HikariDataSource持有HikariPool的兩個引用

在圖中能夠看到,HikariDataSource持有了HikariPool的引用,看過源碼的同窗可能會問,爲何屬性裏會有兩個HikariPool,以下:

public class HikariDataSource extends HikariConfig implements DataSource, Closeable
{
   private final HikariPool fastPathPool;
   private volatile HikariPool pool;
}

這裏補充說明下,其實這裏的兩個HikariPool的不一樣取值表明了不一樣的配置方式:

配置方式一:當經過有參構造new HikariDataSource(HikariConfig configuration)來建立HikariDataSource時,fastPathPool 和 pool 是非空且相同的;

配置方式二:當經過無參構造new HikariDataSource()來建立HikariDataSource並手動配置時,fastPathPool 爲空,pool 不爲空(在第一次 getConnectionI() 時初始化),以下;

public Connection getConnection() throws SQLException
   {
      if (isClosed()) {
         throw new SQLException("HikariDataSource " + this + " has been closed.");
      }

      if (fastPathPool != null) {
         return fastPathPool.getConnection();
      }

      // 第二種配置方式會在第一次 getConnectionI() 時初始化pool
      HikariPool result = pool;
      if (result == null) {
         synchronized (this) {
            result = pool;
            if (result == null) {
               validate();
               LOGGER.info("{} - Starting...", getPoolName());
               try {
                  pool = result = new HikariPool(this);
               }
               catch (PoolInitializationException pie) {
                  if (pie.getCause() instanceof SQLException) {
                     throw (SQLException) pie.getCause();
                  }
                  else {
                     throw pie;
                  }
               }
               LOGGER.info("{} - Start completed.", getPoolName());
            }
         }
      }

      return result.getConnection();
   }

針對以上兩種配置方式,其實使用一個 pool 就能夠完成,那爲何會有兩個?咱們比較下這兩種方式的區別:

private final T t1;
   private volatile T t2;
   public void method01(){
      if (t1 != null) {
         // do something
      }
   }
   public void method02(){
      T result = t2;
      if (result != null) {
         // do something
      }
   }

上面的兩個方法中,執行的代碼幾乎同樣,可是 method02 在性能上會比 method01 稍差。固然,主要問題不是出在 method02 多定義了一個變量,而在於 t2 的 volatile 性質,正由於 t2 被 volatile 修飾,爲了實現數據一致性會出現沒必要要的開銷,因此 method02 在性能上會比 method01 稍差。pool 和 fastPathPool 的問題也是同理,因此,第二種配置方式不建議使用

經過上面的問題就會發現,HiakriCP 在追求性能方面很是重視細節,怪不得可以成爲最快的鏈接池!

HikariPool--管理鏈接的池塘

HikariPool 是一個很是重要的類,它負責管理鏈接,涉及到比較多的代碼邏輯。這裏先簡單介紹下這個類,對下文代碼的具體分析會有所幫助。

<img src="https://img2018.cnblogs.com/blog/1731892/202002/1731892-20200219095716730-255561102.png" alt="HikariPoolUML" />

HikariPool 的幾個屬性說明以下:

屬性類型和屬性名 說明
HikariConfig config 配置信息。
PoolBase.IMetricsTrackerDelegate metricsTracker 指標記錄器包裝類。HikariCP支持Metrics監控,但須要額外引入jar包,本文不會涉及這一部份內容
Executor netTimeoutExecutor 用於執行設置鏈接超時時間的任務。若是是mysql驅動,實現爲PoolBase.SynchronousExecutor,若是是其餘驅動,實現爲ThreadPoolExecutor,爲何mysql不一樣,緣由見:<br>https://bugs.mysql.com/bug.php?id=75615
DataSource dataSource 用於獲取原生鏈接對象的數據源。通常咱們不指定的話,使用的是DriverDataSource
HikariPool.PoolEntryCreator POOL_ENTRY_CREATOR 建立新鏈接的任務,Callable實現類。通常調用一次建立一個鏈接
HikariPool.PoolEntryCreator POST_FILL_POOL_ENTRY_CREATOR 建立新鏈接的任務,Callable實現類。通常調用一次建立一個鏈接,與前者區別在於它建立最後一個鏈接,會打印日誌
Collection<![CDATA[<Runnable>]> addConnectionQueue 等待執行PoolEntryCreator任務的隊列
ThreadPoolExecutor addConnectionExecutor 執行PoolEntryCreator任務的線程池。以addConnectionQueue做爲等待隊列,只開啓一個線程執行任務
ThreadPoolExecutor closeConnectionExecutor 執行關閉原生鏈接的線程池。只開啓一個線程執行任務
ConcurrentBag<![CDATA[<PoolEntry>]> connectionBag 存放鏈接對象的包。用於borrow、requite、add和remove對象。
ProxyLeakTask leakTask 報告鏈接丟棄的任務,Runnable實現類。
SuspendResumeLock suspendResumeLock 基於Semaphore包裝的鎖。若是設置了isAllowPoolSuspension則會生效,默認MAX_PERMITS = 10000
ScheduledExecutorService houseKeepingExecutorService 用於執行HouseKeeper(鏈接檢測任務和維持鏈接池大小)和ProxyLeakTask的任務。只開啓一個線程執行任務
ScheduledFuture<?> houseKeeperTask houseKeepingExecutorService執行HouseKeeper(檢測空閒鏈接任務)返回的結果,經過它能夠結束HouseKeeper任務。

爲了更清晰地理解上面幾個字段的含義,我簡單畫了個圖,不是很嚴謹,將就看下吧。在這個圖中,PoolEntry 封裝了 Connection 對象,在圖中把它當作是鏈接對象會更好理解一些。咱們能夠看到**ConcurrentBag 是整個 HikariPool 的核心**,其餘對象都圍繞着它進行操做,後面會單獨講解這個類。客戶端線程能夠調用它的 borrow、requite 和 remove 方法,houseKeepingExecutorService 線程能夠調用它的 remove 方法,只有 addConnectionExecutor 能夠進行 add 操做。

<img src="https://img2018.cnblogs.com/blog/1731892/202002/1731892-20200219095745457-1165943303.png" alt="HikariPoolSimpleProcess" style="zoom:80%;" />

borrow 和 requite 對於 ConcurrentBag 而言是隻讀的操做,addConnectionExecutor 只開啓一個線程執行任務,因此 add 操做是單線程的,惟一存在鎖競爭的就是 remove 方法。接下來會具體講解 ConcurrentBag

ConcurrentBag--更少的鎖衝突

在 HikariCP 中ConcurrentBag用於存放PoolEntry對象(封裝了Connection對象,IConcurrentBagEntry實現類),本質上能夠將它就是一個資源池。

ConCurrentBag

下面簡單介紹下幾個字段的做用:

屬性 描述
CopyOnWriteArrayList<![CDATA[<T>]> sharedList 存放着狀態爲使用中、未使用和保留三種狀態的PoolEntry對象。注意,CopyOnWriteArrayList是一個線程安全的集合,在每次寫操做時都會採用複製數組的方式來增刪元素,讀和寫使用的是不一樣的數組,避免了鎖競爭。
ThreadLocal<List<![CDATA[<Object>]>> threadList 存放着當前線程返還的PoolEntry對象。若是當前線程再次借用資源,會先從這個列表中獲取。注意,這個列表的元素能夠被其餘線程「偷走」。
SynchronousQueue<![CDATA[<T>]> handoffQueue 這是一個無容量的阻塞隊列,每一個插入操做須要阻塞等待刪除操做,而刪除操做不須要等待,若是沒有元素插入,會返回null,若是設置了超時時間則須要等待。
AtomicInteger waiters 當前等待獲取元素的線程數
IBagStateListener listener 添加元素的監聽器,由HikariPool實現,在該實現中,若是waiting - addConnectionQueue.size() >= 0,則會讓addConnectionExecutor執行PoolEntryCreator任務
boolean weakThreadLocals 元素是否使用弱引用。能夠經過系統屬性com.zaxxer.hikari.useWeakReferences進行設置

這幾個字段在ConcurrentBag中如何使用呢,咱們來看看borrow的方法:

public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException
   {
      // 1. 首先從threadList獲取對象
       
      // 獲取綁定在當前線程的List<Object>對象,注意這個集合的實現通常爲FastList,這是HikariCP本身實現的,後面會講到
      final List<Object> list = threadList.get();
       // 遍歷結合
      for (int i = list.size() - 1; i >= 0; i--) {
         // 獲取當前元素,並將它從集合中刪除
         final Object entry = list.remove(i);
         // 若是設置了weakThreadLocals,則存放的是WeakReference對象,不然爲咱們一開始設置的PoolEntry對象
         @SuppressWarnings("unchecked")
         final T bagEntry = weakThreadLocals ? ((WeakReference<T>) entry).get() : (T) entry;
          // 採用CAS方式將獲取的對象狀態由未使用改成使用中,若是失敗說明其餘線程正在使用它,這裏可知,threadList上的元素能夠被其餘線程「偷走」。
         if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
            return bagEntry;
         }
      }

      // 2.若是還沒獲取到,會從sharedList中獲取對象
       
      // 等待獲取鏈接的線程數+1
      final int waiting = waiters.incrementAndGet();
      try {
         // 遍歷sharedList
         for (T bagEntry : sharedList) {
            // 採用CAS方式將獲取的對象狀態由未使用改成使用中,若是當前元素正在使用,則沒法修改爲功,進入下一循環
            if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
               // 通知監聽器添加包元素。若是waiting - addConnectionQueue.size() >= 0,則會讓addConnectionExecutor執行PoolEntryCreator任務
               if (waiting > 1) {
                  listener.addBagItem(waiting - 1);
               }
               return bagEntry;
            }
         }
         // 通知監聽器添加包元素。
         listener.addBagItem(waiting);
        
         // 3.若是還沒獲取到,會從輪訓進入handoffQueue隊列獲取鏈接對象
         
         timeout = timeUnit.toNanos(timeout);
         do {
            final long start = currentTime();
               // 從handoffQueue隊列中獲取並刪除元素。這是一個無容量的阻塞隊列,插入操做須要阻塞等待刪除操做,而刪除操做不須要等待,若是沒有元素插入,會返回null,若是設置了超時時間則須要等待
            final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS);
            // 這裏會出現三種狀況,
            // 1.超時,返回null
            // 2.獲取到元素,但狀態爲正在使用,繼續執行
            // 3.獲取到元素,元素狀態未未使用,修改未使用並返回
            if (bagEntry == null || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
               return bagEntry;
            }
            // 計算剩餘超時時間
            timeout -= elapsedNanos(start);
         } while (timeout > 10_000);
         // 超時返回null
         return null;
      }
      finally {
         // 等待獲取鏈接的線程數-1
         waiters.decrementAndGet();
      }
   }

在以上方法中,惟一可能出現線程切換到就是handoffQueue.poll(timeout, NANOSECONDS),除此以外,咱們沒有看到任何的 synchronized 和 lock。之因此能夠作到這樣主要因爲如下幾點:

  1. 元素狀態的引入,以及使用CAS方法修改狀態。在ConcurrentBag中,使用使用中、未使用、刪除和保留等表示元素的狀態,而不是使用不一樣的集合來維護不一樣狀態的元素。元素狀態這一律唸的引入很是關鍵,爲後面的幾點提供了基礎。 ConcurrentBag的方法中多處調用 CAS 方法來判斷和修改元素狀態,這一過程不須要加鎖。
  2. threadList 的使用。當前線程歸還的元素會被綁定到ThreadLocal,該線程再次獲取元素時,在該元素未被偷走的前提下可直接獲取到,不須要去 sharedList 遍歷獲取;
  3. 採用CopyOnWriteArrayList來存放元素。在CopyOnWriteArrayList中,讀和寫使用的是不一樣的數組,避免了二者的鎖競爭,至於多個線程寫入,則會加 ReentrantLock 鎖。
  4. sharedList 的讀寫控制。borrow 和 requite 對 sharedList 來講都是不加鎖的,缺點就是會犧牲一致性。用戶線程沒法進行增長元素的操做,只有 addConnectionExecutor 能夠,而 addConnectionExecutor 只會開啓一個線程執行任務,因此 add 操做不會存在鎖競爭。至於 remove 是惟一會形成鎖競爭的方法,這一點我認爲也能夠參照 addConnectionExecutor 來處理,在加入任務隊列前把 PoolEntry 的狀態標記爲刪除中。

其實,咱們會發現,ConcurrentBag在減小鎖衝突的問題上,除了設計改進,還使用了比較多的 JDK 特性。

如何加載配置

在HikariCP 中,HikariConfig用於加載配置,具體的代碼並不複雜,但相比其餘項目,它的加載要更加簡潔一些。咱們直接從PropertyElf.setTargetFromProperties(Object, Properties)方法開始看,以下:

// 這個方法就是將properties的參數設置到HikariConfig中
   public static void setTargetFromProperties(final Object target, final Properties properties)
   {
      if (target == null || properties == null) {
         return;
      }
    
      // 在這裏會利用反射獲取
      List<Method> methods = Arrays.asList(target.getClass().getMethods());
      // 遍歷
      properties.forEach((key, value) -> {
         // 若是是dataSource.*的參數,直接加入到dataSourceProperties屬性
         if (target instanceof HikariConfig && key.toString().startsWith("dataSource.")) {
            ((HikariConfig) target).addDataSourceProperty(key.toString().substring("dataSource.".length()), value);
         }
         else {
            // 若是不是,則經過set方法設置
            setProperty(target, key.toString(), value, methods);
         }
      });
   }

進入到PropertyElf.setProperty(Object, String, Object, List<Method>)方法:

private static void setProperty(final Object target, final String propName, final Object propValue, final List<Method> methods)
   {
      // 拼接參數的setter方法名
      String methodName = "set" + propName.substring(0, 1).toUpperCase(Locale.ENGLISH) + propName.substring(1);
      // 獲取對應的Method 對象
      Method writeMethod = methods.stream().filter(m -> m.getName().equals(methodName) && m.getParameterCount() == 1).findFirst().orElse(null);
      // 若是不存在,按另外一套規則拼接參數的setter方法名
      if (writeMethod == null) {
         String methodName2 = "set" + propName.toUpperCase(Locale.ENGLISH);
         writeMethod = methods.stream().filter(m -> m.getName().equals(methodName2) && m.getParameterCount() == 1).findFirst().orElse(null);
      }
      // 若是該參數setter方法不存在,則拋出異常,從這裏能夠看出,HikariCP 中不能存在配錯參數名的狀況
      if (writeMethod == null) {
         LOGGER.error("Property {} does not exist on target {}", propName, target.getClass());
         throw new RuntimeException(String.format("Property %s does not exist on target %s", propName, target.getClass()));
      }
      
      // 接下來就是調用setter方法來配置具體參數了。
      try {
         Class<?> paramClass = writeMethod.getParameterTypes()[0];
         if (paramClass == int.class) {
            writeMethod.invoke(target, Integer.parseInt(propValue.toString()));
         }
         else if (paramClass == long.class) {
            writeMethod.invoke(target, Long.parseLong(propValue.toString()));
         }
         else if (paramClass == boolean.class || paramClass == Boolean.class) {
            writeMethod.invoke(target, Boolean.parseBoolean(propValue.toString()));
         }
         else if (paramClass == String.class) {
            writeMethod.invoke(target, propValue.toString());
         }
         else {
            writeMethod.invoke(target, propValue);
         }
      }
      catch (Exception e) {
         LOGGER.error("Failed to set property {} on target {}", propName, target.getClass(), e);
         throw new RuntimeException(e);
      }
   }

咱們會發現,相比其餘項目(尤爲是 druid),HikariCP 加載配置的過程很是簡潔,不須要按照參數名一個個地加載,這樣後期會更好維護。固然,這種方式咱們也能夠運用到實際項目中。

獲取一個鏈接對象的過程

如今簡單介紹下獲取鏈接對象的過程,咱們進入到HikariPool.getConnection(long)方法:

public Connection getConnection(final long hardTimeout) throws SQLException
   {  // 若是咱們設置了allowPoolSuspension爲true,則這個鎖會生效
      // 它採用Semaphore實現,MAX_PERMITS = 10000,正常狀況不會用完,除非你掛起了鏈接池(經過JMX等方式),這時10000個permits會一次被消耗完
      suspendResumeLock.acquire();
      // 獲取開始時間
      final long startTime = currentTime();

      try {
         // 剩餘超時時間
         long timeout = hardTimeout;
         PoolEntry poolEntry = null;
         try {
            // 循環獲取,除非獲取到了鏈接或者超時
            do {
               // 從ConcurrentBag中借出一個元素
               poolEntry = connectionBag.borrow(timeout, MILLISECONDS);
               // 前面說過,只有超時狀況纔會返回空,這時會跳出循環並拋出異常
               if (poolEntry == null) {
                  break; 
               }

               final long now = currentTime();
               // 若是元素被標記爲丟棄或者空閒時間過長且鏈接無效則會丟棄該元素,並關閉鏈接
               if (poolEntry.isMarkedEvicted() || (elapsedMillis(poolEntry.lastAccessed, now) > ALIVE_BYPASS_WINDOW_MS && !isConnectionAlive(poolEntry.connection))) {
                  closeConnection(poolEntry, "(connection is evicted or dead)"); // Throw away the dead connection (passed max age or failed alive test)
                  // 計算剩餘超時時間
                  timeout = hardTimeout - elapsedMillis(startTime);
               }
               else {
                  // 這一步用於支持metrics監控,本文不涉及
                  metricsTracker.recordBorrowStats(poolEntry, startTime);
                  // 建立Connection代理類,該代理類就是使用Javassist生成的
                  return poolEntry.createProxyConnection(leakTask.schedule(poolEntry), now);
               }
            } while (timeout > 0L);
            // 不涉及
            metricsTracker.recordBorrowTimeoutStats(startTime);
         }
         catch (InterruptedException e) {
            // 獲取鏈接過程若是中斷,則回收鏈接並拋出異常
            if (poolEntry != null) {
               poolEntry.recycle(startTime);
            }
            Thread.currentThread().interrupt();
            throw new SQLException(poolName + " - Interrupted during connection acquisition", e);
         }
      }
      finally {
         // 釋放一個permit
         suspendResumeLock.release();
      }
      // 拋出超時異常
      throw createTimeoutException(startTime);
   }

以上就是獲取鏈接對象的過程,沒有太複雜的邏輯。這裏須要注意,使用 HikariCP 最好不要開啓 allowPoolSuspension ,不然每次鏈接都會有獲取和釋放 permit 的過程。另外,HikariCP 默認 testOnBorrow,有點難以理解。

以上,HikariCP 的使用例子和源碼分析基本講完,後續有空再作補充。

參考資料

微信公衆號【工匠小豬豬的技術世界】的追光者系列文章

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

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

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

相關文章
相關標籤/搜索