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


簡介

HikariCP是什么?

HikariCP 本質上就是一個數據庫連接池。

HikariCP 解決了哪些問題?

創建和關閉數據庫連接的開銷很大,HikariCP 通過“池”來復用連接,減小開銷。

為什么要使用 HikariCP?

  1. HikariCP 是目前最快的連接池。就連風靡一時的 boneCP 也停止維護,主動讓位給它。SpringBoot 也把它設置為默認連接池。
  1. HikariCP 非常輕量。本文用到的 4.0.3 版本的 jar 包僅僅只有 156 KB,它的源碼真的非常精煉。

本文要講什么?

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

  1. 如何使用 HikariCP(入門、JMX 等)
  2. 配置參數詳解
  3. 源碼分析

如何使用 HikariCP

需求

使用 HikariCP 獲取連接對象,對用戶數據進行簡單的增刪改查。

項目環境

JDK:1.8.0_231

maven:3.6.3

IDE:Spring Tool Suite 4.6.1.RELEASE

mysql-connector-java:8.0.15

mysql:5.7.28

Hikari:4.0.3

引入依賴

項目類型 Maven Project,打包方式 jar。

 		<!-- test -->
        <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>4.0.3</version>
        </dependency>
        <!-- mysql驅動 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.15</version>
        </dependency>
        <!-- log -->
        <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

本文使用配置文件的方式來配置 HikariCP,當然,我們也可以在代碼中顯式配置,但不提倡。因為是入門例子,這里我只給出了必需的參數,其他的參數后面會詳細介紹。

jdbcUrl=jdbc:mysql://localhost:3306/github_demo?characterEncoding=utf8&serverTimezone=GMT%2B8
username=root
password=root

初始化連接池

初始化連接池時,我們可以在代碼中顯式指定配置文件,也可以通過啟動參數配置。

// 加載配置文件,也可以無參構造並使用啟動參數 hikaricp.configurationFile 指定配置文件(不推薦,后面會說原因)
HikariConfig config = new HikariConfig("/hikari2.properties");
HikariDataSource dataSource = new HikariDataSource(config);

初始化連接池之后,我們可以通過HikariDataSource.getConnection()方法獲取連接對象,然后進行增刪改查操作,這部分內容這里就不展示了。

如何使用 JMX 管理連接池

需求

開啟 JMX 功能,並使用 jconsole 管理連接池。

開啟JMX

在入門例子的基礎上增加配置。這要設置 registerMbeans 為 true,JMX 功能就會開啟。

#-------------JMX--------------------------------
# 是否開啟 JMX
# 默認 false
registerMbeans=true

# 是否允許通過 JMX 掛起和恢復連接池
# 默認為 false
allowPoolSuspension=true

# 連接池名稱。
# 默認自動生成
poolName=zzsCP

啟動連接池

為了查看具體效果,這里讓主線程進入睡眠 20 分鍾。

    public static void main(String[] args) throws InterruptedException {
        HikariConfig config = new HikariConfig("/hikaricp_base.properties");
        HikariDataSource dataSource = new HikariDataSource(config);
        Thread.sleep(20 * 60 * 1000);
        dataSource.close();
    }

使用jconsole查看

運行 main 方法,使用 JDK 的工具 jconsole 連接我們的項目,在 MBean 選項卡可以看到我們的連接池。接下里,我們可以進行這樣的操作:

  1. 通過 PoolConfig 動態修改配置(只有部分參數允許修改);
  2. 通過 Pool 獲取連接池的連接數(活躍、空閑和所有)、獲取等待連接的線程數、掛起和恢復連接池、丟棄未使用連接等。

hikaricp_jmx

想了解更多 JMX 功能可以參考我的博客文章: 如何使用JMX來管理程序?

配置參數詳解

相比其他連接池,HikariCP 的配置參數非常簡單,其中有幾個功能需要注意:

  1. HikariCP 借出連接時強制檢查連接的活性,不像其他連接池一樣可以選擇不檢查;
  2. 默認會檢查 idleTimeout、maxLifetime,可以選擇禁用,但不推薦;
  3. 默認不檢查 keepaliveTime、leakDetectionThreshold,可以選擇開啟,推薦開啟 leakDetectionThreshold 即可。

必需的參數

注意,這里 jdbcUrl 和 dataSourceClassName 二選一。

#-------------必需的參數--------------------------------
# JDBC 驅動中 DataSource 的實現類全限定類名。不支持 XA DataSource
# 如果指定, HikariCP 將使用 DataSouce.getConnection 獲取連接而不是使用 DriverManager.getConnection,官方建議指定(mysql 除外)
# dataSourceClassName=

# 如果指定, HikariCP 將使用 DriverManager.getConnection 獲取連接而不是使用 DataSouce.getConnection
jdbcUrl=jdbc:mysql://localhost:3306/github_demo?characterEncoding=utf8&serverTimezone=GMT%2B8

# 用戶名和密碼
username=root
password=root

常用的參數

# 從池中借出的連接是否默認自動提交事務
# 默認 true
autoCommit=true

# 當我從池中借出連接時,願意等待多長時間。如果超時,將拋出 SQLException
# 默認 30000 ms,最小值 250 ms。支持 JMX 動態修改
connectionTimeout=30000

# 一個連接在池里閑置多久時會被拋棄
# 當 minimumIdle < maximumPoolSize 才生效
# 默認值 600000 ms,最小值為 10000 ms,0表示禁用該功能。支持 JMX 動態修改
idleTimeout=600000

# 多久檢查一次連接的活性
# 檢查時會先把連接從池中拿出來(空閑的話),然后調用isValid()或執行connectionTestQuery來校驗活性,如果通過校驗,則放回池里。
# 默認 0 (不啟用),最小值為 30000 ms,必須小於 maxLifetime。支持 JMX 動態修改
keepaliveTime=0

# 當一個連接存活了足夠久,HikariCP 將會在它空閑時把它拋棄
# 默認 1800000  ms,最小值為 30000 ms,0 表示禁用該功能。支持 JMX 動態修改
maxLifetime=1800000

# 用來檢查連接活性的 sql,要求是一個查詢語句,常用select 'x'
# 如果驅動支持 JDBC4.0,建議不設置,這時默認會調用  Connection.isValid() 來檢查,該方式會更高效一些
# 默認為空
# connectionTestQuery=

# 池中至少要有多少空閑連接。
# 當空閑連接 < minimumIdle,總連接 < maximumPoolSize 時,將新增連接
# 默認等於 maximumPoolSize。支持 JMX 動態修改
minimumIdle=5

# 池中最多容納多少連接(包括空閑的和在用的)
# 默認為 10。支持 JMX 動態修改
maximumPoolSize=10

# 用於記錄連接池各項指標的 MetricRegistry 實現類
# 默認為空,只能通過代碼設置
# metricRegistry=

# 用於報告連接池健康狀態的 HealthCheckRegistry 實現類
# 默認為空,只能通過代碼設置
# healthCheckRegistry=

# 連接池名稱。
# 默認自動生成
poolName=zzsCP

很少用的參數

# 如果啟動連接池時不能成功初始化連接,是否快速失敗 TODO
# >0 時,會嘗試獲取連接。如果獲取時間超過指定時長,不會開啟連接池,並拋出異常
# =0 時,會嘗試獲取並驗證連接。如果獲取成功但驗證失敗則不開啟池,但是如果獲取失敗還是會開啟池
# <0 時,不管是否獲取或校驗成功都會開啟池。
# 默認為 1
initializationFailTimeout=1

# 是否在事務中隔離 HikariCP 自己的查詢。
# autoCommit 為 false 時才生效
# 默認 false
isolateInternalQueries=false

# 是否允許通過 JMX 掛起和恢復連接池
# 默認為 false
allowPoolSuspension=false

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

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

# 數據庫 catalog
# 默認由驅動決定
# catalog=

# 在每個連接創建后、放入池前,需要執行的初始化語句
# 如果執行失敗,該連接會被丟棄
# 默認為空
# connectionInitSql=

# JDBC 驅動使用的 Driver 實現類
# 一般根據 jdbcUrl 判斷就行,報錯說找不到驅動時才需要加
# 默認為空
# driverClassName=

# 連接的默認事務隔離級別
# 默認值為空,由驅動決定
# transactionIsolation=

# 校驗連接活性允許的超時時間
# 默認 5000 ms,最小值為 250 ms,要求小於 connectionTimeout。支持 JMX 動態修改
validationTimeout=5000

# 連接對象可以被借出多久
# 默認 0(不開啟),最小允許值為 2000 ms。支持 JMX 動態修改
leakDetectionThreshold=0

# 直接指定 DataSource 實例,而不是通過 dataSourceClassName 來反射構造
# 默認為空,只能通過代碼設置
# dataSource=

# 數據庫 schema
# 默認由驅動決定
# schema=

# 指定連接池獲取線程的 ThreadFactory 實例
# 默認為空,只能通過代碼設置
# threadFactory=

# 指定連接池開啟定時任務的 ScheduledExecutorService 實例(建議設置setRemoveOnCancelPolicy(true))
# 默認為空,只能通過代碼設置
# scheduledExecutor=

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

源碼分析

HikariCP 的源碼少且精,可讀性非常高。如果你沒見過像詩一樣的代碼,可以來看看 HikariCP。

提醒一下,在閱讀 HiakriCP 源碼之前,需要掌握CopyOnWriteArrayListAtomicIntegerSynchronousQueueSemaphoreAtomicIntegerFieldUpdaterLockSupport等 JDK 自帶類的使用。

注意,考慮到篇幅和可讀性,以下代碼經過刪減。

HikariCP為什么快?

數據庫連接池已經發展了很久了,也算是比較成熟的技術,使用比較廣泛的類庫有 boneCP、DBCP、C3P0、Druid 等等。眼看着數據庫連接池已經發展到了瓶頸,所謂的性能提升也僅僅是一些代碼細節的優化,這個時候,HikariCP 出現並快速地火了起來,與其他連接池相比,它的快不是普通的快,而是跨越性的快。下面是 JMH 測試的結果([測試項目地址](brettwooldridge/HikariCP-benchmark: JHM benchmarks for JDBC Connection Pools (github.com)))。

DataSource_performance_test

HikariCP 為什么快?我看網上有很多的解釋,例如,大量使用 JDK 並發包的工具來避免粗顆粒度的鎖、FastList 等自定義類的使用、動態代理類等等。我覺得,這些都不是主要原因。

HikariCP 之所以快,更多的還是由於抽象層面的優化

傳統模型--中規中矩的模型

連接池,顧名思義,就是一個存放連接對象的池塘。幾乎所有的連接池都會從代碼層面抽象出一個池塘。池里的連接數量不是一成不變的,例如,連接失效了需要移除、新連接創建、用戶借出或歸還連接,等等,總結起來,對連接池的操作不外乎四個:borrow、return、add、remove

連接池一般是這樣設計的:borrow、remove 動作會將連接從池塘里拿出來,add、return 動作則會往池塘里添加連接。我把這種模型稱為“傳統模型”。

hikaricp_pool01

“傳統模型”是比較中規中矩的模型,從抽象層面講,它非常符合我們的現實生活,例如,某人借走我的錢,錢就不在我的錢包里了。我們熟知的 DBCP、C3P0、Druid 等等都是基於“傳統模型”開發的。

標記模型--更少的鎖

但是 HikariCP 就不一樣了,它沒有走老路,而是優化了“傳統模型”,讓連接池真正意義地實現了提速。

在“傳統模型”中,borrow、return、add、remove 四個動作都需要加同一把鎖,即同一時刻只允許一個線程操作池,並發高時線程切換將非常頻繁。因為多個線程操作同一個池塘,連接出入池需要加鎖來保證線程安全。針對這一點,我們是不是能做些什么呢?

HikariCP 是這樣做的,borrow 的連接不會從池塘里取出,而是打上“已借出”的標記,return 的時候,再把這個連接的“已借出”標記去掉。我把這種做法稱為“標記模型”。“標記模型”可以實現 borrow 和 return 動作不加鎖。具體怎么做到的呢?

hikaricp_pool02

首先,我要 borrow 時,我需要看看池塘里哪一個連接可以借出。這里就涉及到讀連接池的操作,因為池塘里的連接數量不是一成不變的,為了一致性,我們就必須加鎖。但是,HikariCP 沒有加,為什么呢?因為 HikariCP 容忍了讀的不一致。borrow 的時候,我們實際上讀的不是真正的池塘,而是當前池塘的一份快照。我們看看 HikariCP 存放連接的地方,是一個CopyOnWriteArrayList對象,我們知道,CopyOnWriteArrayList是一個寫安全、讀不安全的集合。

public class ConcurrentBag<T extends IConcurrentBagEntry> implements AutoCloseable {
   // 存放連接的集合
   private final CopyOnWriteArrayList<T> sharedList;
}

接着,當我們找到了一個可借出的連接時,需要給它打上借出的標記。注意,這時有可能出現多個線程都想給它打標記的情況,該怎么辦呢?難道要加鎖了嗎?別忘了我們可以用 CAS 機制來更新連接的標記,這個時候就不需要加鎖了。看看 HikariCP 就是這么實現的。

   public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException {
      final int waiting = waiters.incrementAndGet();
      try {
         for (T bagEntry : sharedList) {
            if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
               return bagEntry;
            }
         }


         return null;
      }
      finally {
         waiters.decrementAndGet();
      }
   }

於是,在“標記模型”里,只有 add 和 remove 才需要加鎖,borrow 和 return 不需要加鎖。通過這種顛覆式的設計,連接池的性能得到極大的提高。

這就是我認為的 HikariCP 快的最主要原因。

HikariCP主要的類

那么,我們來看看 HikariCP 的源碼吧。

HikariCP 的類不多,最主要的就這幾個,如圖。不難發現,這種類結構和 DBCP2 很像。

這幾個類可以分成四個部分:

  1. 用戶接口。用戶一般會使用DataSource.getConnection()來獲取連接對象。
  2. JMX 支持。
  3. 配置信息。使用HikariConfig加載配置文件,或手動配置HikariConfig的參數,它一般會作為入參來構造HikariDataSource對象;
  4. 連接池。獲取連接的過程為HikariDataSource.getConnection()->HikariPool.getConnection()->ConcurrentBag.borrow(long, TimeUnit)需要注意的是,ConcurrentBag才是真正的連接池,而HikariPool是用來管理連接池的

ConcurrentBag--最核心的類

ConcurrentBag可以算是 HikariCP 最核心的一個類,它是 HikariCP 底層真正的連接池,上面說的“標記模型”只要就是靠它來實現的。如果大家不想看太多代碼的話,只看它就足夠了。

在設計上,ConcurrentBag是一個比較通用的資源池,它可以是數據庫連接的池,也可以是其他對象的池,只要存放的資源對象實現了IConcurrentBagEntry接口即可。所以,如果我們的項目中需要自己構建池的話,可以直接拿這個現成的組件來用。

public class ConcurrentBag<T extends IConcurrentBagEntry> implements AutoCloseable {
	private final CopyOnWriteArrayList<T> sharedList;
}

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

ConCurrentBagUml

屬性 描述
CopyOnWriteArrayList sharedList 存放着狀態為使用中、未使用和保留三種狀態的資源對象
ThreadLocal threadList 存放着當前線程歸還的資源對象
SynchronousQueue handoffQueue 這是一個無容量的阻塞隊列,出隊和入隊都可以選擇是否阻塞
AtomicInteger waiters 當前等待獲取元素的線程數

這幾個字段在ConcurrentBag中如何使用呢,這里拿borrow方法來說明下:

   public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException
   {
      // 1. 首先從threadList獲取資源
       
      final List<Object> list = threadList.get();
      for (int i = list.size() - 1; i >= 0; i--) {
         final Object entry = list.remove(i);
         final T bagEntry = weakThreadLocals ? ((WeakReference<T>) entry).get() : (T) entry;
         if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
            return bagEntry;
         }
      }

      // 等待獲取連接的線程數+1
      final int waiting = waiters.incrementAndGet();
      try {
         // 2.如果還沒獲取到,會從sharedList中獲取對象
         for (T bagEntry : sharedList) {
            if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
               // 這一步我不是很懂,好像可有可無
               if (waiting > 1) {
                  listener.addBagItem(waiting - 1);
               }
               return bagEntry;
            }
         }
         // 從sharedList中獲取不到資源,通知監聽器創建資源(不一定會創建)
         listener.addBagItem(waiting);
        
         // 3.如果還沒獲取到,會堵塞等待空閑連接
         timeout = timeUnit.toNanos(timeout);
         do {
            final long start = currentTime();
            // 這里會出現三種情況,
            // 1.超時,返回null
            // 2.獲取到資源,但狀態為正在使用,繼續循環
            // 3.獲取到資源,元素狀態為未使用,修改為已使用並返回
            final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS);
            if (bagEntry == null || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
               return bagEntry;
            }
            timeout -= elapsedNanos(start);
         } while (timeout > 10_000);
         // 4.超時了還是沒有獲取到,返回null
         return null;
      }
      finally {
         // 等待獲取連接的線程數-1
         waiters.decrementAndGet();
      }
   }

在上面的方法中,唯一會造成線程阻塞的就是handoffQueue.poll(timeout, NANOSECONDS),除此之外,我們沒有看到任何的 synchronized 和 lock。

HikariPool--管理連接池

除了ConcurrentBagHikariPool也是一個比較重要的類,它用來管理連接池

HikariPoolUML

HikariPool 的幾個字段說明如下:

屬性類型和屬性名 說明
DataSource dataSource 用於獲取原生連接對象的數據源。一般我們不指定的話,使用的是DriverDataSource
ThreadPoolExecutor addConnectionExecutor 執行創建連接任務的線程池。只開啟一個線程執行任務
ThreadPoolExecutor closeConnectionExecutor 執行關閉原生連接任務的線程池。只開啟一個線程執行任務
ScheduledExecutorService houseKeepingExecutorService 用於執行檢查 idleTimeout、leakDetectionThreshold、keepaliveTime、maxLifetime 等任務的線程池。

為了更清晰地理解上面幾個字段的含義,我簡單畫了個圖,不是很嚴謹,將就看下吧。在這個圖中,客戶端線程可以調用進行 borrow、requite 和 remove 操作,houseKeepingExecutorService 線程可以調用進行 remove 操作,只有 addConnectionExecutor 可以進行 add 操作。

HikariPoolSimpleProcess

一些有趣的地方

掌握了上面的兩個類,HikariCP 的整個源碼視圖應該就比較完整了。下面再說一些有趣的地方。

為什么HikariDataSource有兩個HikariPool

在下面的代碼中,HikariDataSource里竟然有兩個HikariPool

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

為什么要這樣做呢?

首先,從性能方面考慮,使用 fastPathPool 來創建連接會比 pool 更好一些,因為 pool 被 volatile 修飾了,為了保證可見性不能使用緩存。那為什么還要用到 pool 呢?

我們打開HikariDataSource.getConnection(),可以看到,pool 的存在可以用來支持雙重檢查鎖。這里我比較好奇的是,為什么不把 HikariPool的引用給 fastPathPool??這個問題大家感興趣可以研究一下。

   public Connection getConnection() throws SQLException
   {
      if (fastPathPool != null) {
         return fastPathPool.getConnection();
      }

      HikariPool result = pool;
      if (result == null) {
         synchronized (this) {
            result = pool;
            if (result == null) {
               validate();
               pool = result = new HikariPool(this);
               this.seal();
            }
         }
      }

      return result.getConnection();
   }

其實,這兩個HikariPool對象有兩種取值情況:

取值一:fastPathPool = pool = new HikariPool(this)。當通過有參構造new HikariDataSource(HikariConfig configuration)來創建HikariDataSource就會出現這樣取值;

取值二:fastPathPool = null;pool = new HikariPool(this)。當通過無參構造new HikariDataSource()來創建HikariDataSource就會出現這樣取值。

所以,我更推薦使用new HikariDataSource(HikariConfig configuration)的方式,因為這樣做的話,我們將使用 fastPathPool 來獲取連接。

如何加載配置

HikariCP 加載配置的代碼非常簡潔。我們直接從PropertyElf.setTargetFromProperties(Object, Properties)方法開始看,如下。

   // 這個方法就是將properties的參數設置到HikariConfig中
   public static void setTargetFromProperties(final Object target, final Properties properties)
   {
      if (target == null || properties == null) {
         return;
      }
    
      // 獲取HikariConfig的所有方法
      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 {
            // 找到參數對應的setter方法並賦值
            setProperty(target, key.toString(), value, methods);
         }
      });
   }

相比其他類庫(尤其是 druid),HikariCP 加載配置的過程非常簡潔,不需要按照參數名一個個地加載,這樣后期會更好維護。當然,這種方式我們也可以運用到實際項目中。

另外,配置 HikariCP 的時候不允許寫錯參數或者添加一些無關的參數,否則會因為找不到對應的 setter 方法而報錯。

以上基本講完 HikariCP 的源碼。后續發現其他有趣的地方再做補充,也歡迎大家指正不足的地方。

最后,感謝閱讀。

參考資料

HikariCP github

2021-05-20 修改

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

本文為原創文章,轉載請附上原文出處鏈接: https://www.cnblogs.com/ZhangZiSheng001/p/12329937.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM