簡介
HikariCP是什么?
HikariCP 本質上就是一個數據庫連接池。
HikariCP 解決了哪些問題?
創建和關閉數據庫連接的開銷很大,HikariCP 通過“池”來復用連接,減小開銷。
為什么要使用 HikariCP?
- HikariCP 是目前最快的連接池。就連風靡一時的 boneCP 也停止維護,主動讓位給它。SpringBoot 也把它設置為默認連接池。

- HikariCP 非常輕量。本文用到的 4.0.3 版本的 jar 包僅僅只有 156 KB,它的源碼真的非常精煉。
本文要講什么?
本文將包含以下內容(因為篇幅較長,可根據需要選擇閱讀):
- 如何使用 HikariCP(入門、JMX 等)
- 配置參數詳解
- 源碼分析
如何使用 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 選項卡可以看到我們的連接池。接下里,我們可以進行這樣的操作:
- 通過 PoolConfig 動態修改配置(只有部分參數允許修改);
- 通過 Pool 獲取連接池的連接數(活躍、空閑和所有)、獲取等待連接的線程數、掛起和恢復連接池、丟棄未使用連接等。
想了解更多 JMX 功能可以參考我的博客文章: 如何使用JMX來管理程序?
配置參數詳解
相比其他連接池,HikariCP 的配置參數非常簡單,其中有幾個功能需要注意:
- HikariCP 借出連接時強制檢查連接的活性,不像其他連接池一樣可以選擇不檢查;
- 默認會檢查 idleTimeout、maxLifetime,可以選擇禁用,但不推薦;
- 默認不檢查 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 源碼之前,需要掌握CopyOnWriteArrayList
、AtomicInteger
、SynchronousQueue
、Semaphore
、AtomicIntegerFieldUpdater
、LockSupport
等 JDK 自帶類的使用。
注意,考慮到篇幅和可讀性,以下代碼經過刪減。
HikariCP為什么快?
數據庫連接池已經發展了很久了,也算是比較成熟的技術,使用比較廣泛的類庫有 boneCP、DBCP、C3P0、Druid 等等。眼看着數據庫連接池已經發展到了瓶頸,所謂的性能提升也僅僅是一些代碼細節的優化,這個時候,HikariCP 出現並快速地火了起來,與其他連接池相比,它的快不是普通的快,而是跨越性的快。下面是 JMH 測試的結果([測試項目地址](brettwooldridge/HikariCP-benchmark: JHM benchmarks for JDBC Connection Pools (github.com)))。

HikariCP 為什么快?我看網上有很多的解釋,例如,大量使用 JDK 並發包的工具來避免粗顆粒度的鎖、FastList 等自定義類的使用、動態代理類等等。我覺得,這些都不是主要原因。
HikariCP 之所以快,更多的還是由於抽象層面的優化。
傳統模型--中規中矩的模型
連接池,顧名思義,就是一個存放連接對象的池塘。幾乎所有的連接池都會從代碼層面抽象出一個池塘。池里的連接數量不是一成不變的,例如,連接失效了需要移除、新連接創建、用戶借出或歸還連接,等等,總結起來,對連接池的操作不外乎四個:borrow、return、add、remove。
連接池一般是這樣設計的:borrow、remove 動作會將連接從池塘里拿出來,add、return 動作則會往池塘里添加連接。我把這種模型稱為“傳統模型”。

“傳統模型”是比較中規中矩的模型,從抽象層面講,它非常符合我們的現實生活,例如,某人借走我的錢,錢就不在我的錢包里了。我們熟知的 DBCP、C3P0、Druid 等等都是基於“傳統模型”開發的。
標記模型--更少的鎖
但是 HikariCP 就不一樣了,它沒有走老路,而是優化了“傳統模型”,讓連接池真正意義地實現了提速。
在“傳統模型”中,borrow、return、add、remove 四個動作都需要加同一把鎖,即同一時刻只允許一個線程操作池,並發高時線程切換將非常頻繁。因為多個線程操作同一個池塘,連接出入池需要加鎖來保證線程安全。針對這一點,我們是不是能做些什么呢?
HikariCP 是這樣做的,borrow 的連接不會從池塘里取出,而是打上“已借出”的標記,return 的時候,再把這個連接的“已借出”標記去掉。我把這種做法稱為“標記模型”。“標記模型”可以實現 borrow 和 return 動作不加鎖。具體怎么做到的呢?

首先,我要 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 很像。

這幾個類可以分成四個部分:
- 用戶接口。用戶一般會使用
DataSource.getConnection()
來獲取連接對象。 - JMX 支持。
- 配置信息。使用
HikariConfig
加載配置文件,或手動配置HikariConfig
的參數,它一般會作為入參來構造HikariDataSource
對象; - 連接池。獲取連接的過程為
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
的幾個字段:
屬性 | 描述 |
---|---|
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--管理連接池
除了ConcurrentBag
,HikariPool
也是一個比較重要的類,它用來管理連接池。
HikariPool 的幾個字段說明如下:
屬性類型和屬性名 | 說明 |
---|---|
DataSource dataSource | 用於獲取原生連接對象的數據源。一般我們不指定的話,使用的是DriverDataSource |
ThreadPoolExecutor addConnectionExecutor | 執行創建連接任務的線程池。只開啟一個線程執行任務。 |
ThreadPoolExecutor closeConnectionExecutor | 執行關閉原生連接任務的線程池。只開啟一個線程執行任務。 |
ScheduledExecutorService houseKeepingExecutorService | 用於執行檢查 idleTimeout、leakDetectionThreshold、keepaliveTime、maxLifetime 等任務的線程池。 |
為了更清晰地理解上面幾個字段的含義,我簡單畫了個圖,不是很嚴謹,將就看下吧。在這個圖中,客戶端線程可以調用進行 borrow、requite 和 remove 操作,houseKeepingExecutorService 線程可以調用進行 remove 操作,只有 addConnectionExecutor 可以進行 add 操作。

一些有趣的地方
掌握了上面的兩個類,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 的源碼。后續發現其他有趣的地方再做補充,也歡迎大家指正不足的地方。
最后,感謝閱讀。
參考資料
2021-05-20 修改
本文為原創文章,轉載請附上原文出處鏈接: https://www.cnblogs.com/ZhangZiSheng001/p/12329937.html