在上一篇 《Java 資料持久化系列之池化技術》
中,我們了解了池化技術,并使用 Apache-common-Pool2 實作了一個簡單連接配接池,實驗對比了它和 HikariCP、Druid 等資料庫連接配接池的性能資料。在性能方面,HikariCP遙遙領先,而且它還是 Spring Boot 2.0 預設的資料庫連接配接池。下面我們就來了解一下這款明星級開源資料庫連接配接池的實作。
本文的主要内容包括:
- HikariCP 簡介,介紹它的特性和現況。
- HikariCP 的配置項詳解,分析部配置設定置的影響。
- HikariCP 為什麼這麼快,介紹其優化點。

這裡啰嗦兩句,由于本系列會涉及很多開源項目,比如說 HikariCP、Druid、Mybatis等,是以簡單聊一下我對學習開源項目的認識,這也是我自己行文或者組織系列文章順序的思路,後續有時間再詳細總結一下。
- 安裝并檢查提供的所有工具,比如 Redis 目錄下的 redis-check-aof 等工具的作用,這些工具都是官方特意提供的,一般都是日常經常要使用的,了解其功能。
- 運作,學習所有配置項的功能,原理和優缺點,比如 Redis 的記憶體溢出控制政策 maxmemory-policy 的可選值都有哪些,分别對應的政策是什麼含義,适用于哪些場景等。
- 原理研究,針對關鍵特性進行研究,比如 Netty 的異步 NIO 和零拷,HikariCP的高并發
- 優缺點對比,同類型開源産品對比,一般某一領域的開源項目往往有多個,比如說 Redis 和 Memcache,Kafka 和 RocketMQ,這些項目之間往往各有優劣,适用場景,了解了這些,也往往進一步加深了對項目關鍵特性和原理的研究。
- demo或者性能測試,按照自己的使用場景去進行 Demo 驗證和性能測試
- 根據demo來檢視調用棧,閱讀關鍵源碼,帶着問題去閱讀源碼,比如閱讀 Redis 如何進行 aof 持久化等。
- 試圖修改源碼,隻是閱讀源碼其實很多時候無法體會到代碼為什麼實作成這樣,在有餘力的情況下修改源碼,比較實作方案,可以更好的了解實作方案,并未後續成為 commiter 打下基礎。
HikariCP 簡介
Hikari 在日語中的含義是光,作者特意用這個含義來表示這塊資料庫連接配接池真的速度很快。官方位址是
https://github.com/brettwooldridge/HikariCP。
Hikari 最引以為傲的就是它的性能,是以作者也在貼下了很多性能資料和使用者回報。筆者也在上一篇文章中使用它的 benchmark 進行了性能對比。
從上圖中可以直覺的看出,Hikari 在 擷取和釋放 Connection 和 Statement 方法的 OPS 不是一般的高,那是相當的高,基本上是碾壓其他連接配接池,這裡就不一一點名了。
除了 OPS 外,HikariCP 的穩定性也更好,性能毛刺更少。
除了性能之外,HikariCP 在很多編碼細節上也下了很多功夫。
比如說使用 JDBC4Connection 的 isValid 函數來檢查 Connection 有效性,該函數使用原生的 ping 指令檢查,比一般資料庫連接配接池預設使用的 select 1 語句快一倍,性能更好。
更加遵循 JDBC 規範,在關閉 Connection 之前先關閉與之關聯的 Statement 和ResultSet 等。對 JDBC 不了解的同學可以閱讀本系列中第一篇
文章對于資料庫連接配接中斷的情況,HikariCP 也處理的更加出色。作者做了實驗,通過測試擷取 Connection 的逾時場景,各個資料庫都設定了跟連接配接逾時 connectionTimeout 類似的參數為 5 秒鐘。其中 HikariCP 等待5秒鐘後,如果連接配接還是沒有恢複,則抛出一個SQLExceptions 異常,後續再擷取 Connection 也是一樣處理。其他資料庫連接配接池的處理則不理想,要麼是一直等到 TCP 逾時才響應,比如 Dbcp2 和 C3PO,要麼是需要修改預設配置,比如說 Vibur。
具體文章可以閱讀 《Bad Behavior: Handling Database Down》一文(連結在文末)。
配置詳解
下面,我們來詳細了解一下 HikariCP 的相關配置。
首先,Spring Boot 2.0 的預設資料庫連接配接池配置就是 HikariCP,是以你無需引入其他依賴,直接在 yml 檔案中進行 HikariCP 的相關配置即可。基礎配置如下所示。
spring:
datasource:
hikari:
minimum-idle: 20
maximum-pool-size: 100
pool-name: dbcp1
idle-timeout: 10000
### Driver 類名和 資料庫 URL,使用者名密碼等 datasource 基礎配置
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3300/test?rewriteBatchedStatements=true&autoReconnect=true&useSSL=false&useUnicode=true&characterEncoding=utf-8
username: ${AUTH_DB_PWD:root}
password: ${AUTH_DB_USER:test}
### 顯示指定資料庫連接配接池,預設也是 HikariDataSource,指定資料庫連接配接池
type: com.zaxxer.hikari.HikariDataSource
HikariCP 的所有配置及其預設值可以在 HikariConfig 中檢視,下面我們來依次介紹較為常用的配置。
- autoCommit:控制從資料庫連接配接池傳回的 Connection 是否預設事務自動送出行為,預設為 true。
- connectionTimeout:控制用戶端在擷取池中 Connection 的等待時間,如果沒有連接配接可用的情況下超過該時間,則抛出 SQLException 異常,比如說 getConnection時連接配接數已經大于 maximumPoolSize 并且一直沒有空閑的連接配接 。預設 30 s。
- idleTimeout:控制 Connection 閑置在池中的最大時間。當 minimumIdle 值大于 maximumPoolSize 小時才生效,而且隻有當池中 Connection 數量大于 minimumIdle 時才根據該時間進行 Connection 剔除。預設為 600000 s(10 分鐘)。
- maxLifetime:控制池中 Connection 的最大生命周期。處于使用中的 Connection 不會因為自身生命超出該時間而被剔除,隻有等到被歸還關閉後才會被剔除。HikariCP 作者強烈建議使用者設定該值,并且它應該比任何資料庫服務的連接配接事件限制短幾秒。預設為 1800000 s(30分鐘)。
- connectionTestQuery:控制資料庫連接配接池借出 Connection 前對其進行檢查,如果使用的 Driver 是 JDBC4 則不建議設定該屬性。不配做會使用 ping 指令進行檢查,其性能大緻為 select 1 的1倍左右。預設為無。
- minimumIdle:控制池中維護的空閑 Connection 的最小數量。如果空閑連接配接數大小該數值,并且總連接配接數小于 maximumPoolSize,則 HikariCP 将盡力快速添加新的 Connection。預設等于 maximumPoolSize。
- maximumPoolSize:控制資料庫連接配接池 Connection 的最大數量,包括空閑和正在使用的。
對于 minimumIdle 和 maximumPoolSize 對資料庫連接配接數量的影響如下圖所示,當 minimumIdle 小于 maximumPoolSize 時,連接配接數量會在該區間内變化,空閑時間超過 idleTimeout 的連接配接會被剔除,直到數量變為 minimumIdle 位置。
但是 HikariCP 的作者建議不設定 minimumIdle,或将其設定為maximumPoolSize 相同數值(預設也是如此),将 HikariCP 充當一個固定大小的連接配接池使用,這樣可以最大限度提高性能和對突發流量的相應能力。
HikariCP 對于這些配置的預設值都進行最優配置,使用時往往不需要調整。但是使用場景千變萬化,有些情況下還是需要根據自己的情況進行調整,後續文章會對較為重要的幾個屬性的影響和調整技巧做詳細的說明。
為什麼這麼快
官網詳細地說明了 HikariCP 所做的一些優化,總結如下:
- 位元組碼精簡 :優化代碼,直到編譯後的位元組碼最少,這樣,CPU 緩存可以加載更多的程式代碼;
- 優化代理和攔截器:減少代碼,例如 HikariCP 的 Statement proxy 隻有100行代碼,隻有 BoneCP 的十分之一;
- 自定義的 FastList 代替 ArrayList:避免每次 get 調用都要進行 range check,避免調用 remove 時的從頭到尾的掃描;
- 自定義集合類型 ConcurrentBag,提高并發讀寫的效率;
- 其他針對 BoneCP 缺陷的優化,比如對于耗時超過一個 CPU 時間片的方法調用的研究(但沒說具體怎麼優化)
HikariCP 具體的優化細節可以閱讀作者寫的《Down the Rabbit Hole》一文(位址連結在文末),Rabbit Hole 是指兔子洞,寓意是複雜奇藝且未知的境地,來自愛麗絲漫遊奇境記中愛麗絲掉入兔子洞。
下面我們就簡單說明一下幾項優化。
使用 FastList 替代 ArrayList
HikariCP 通過分析 Connection 使用 Statement 的場景,提出了使用 FastList 代替 ArrayList 的優化方案。
FastList 是一個 List 接口的精簡實作,隻實作了接口中必要的幾個方法。它主要做了如下幾點優化:
- ArrayList 每次調用 get 方法時都會進行 rangeCheck 檢查索引是否越界,其實隻要保證索引合法那麼 rangeCheck 就成為不必要的計算開銷。是以,FastList 不會進行該檢查。
- ArrayList 的 remove(Object) 方法是從頭開始周遊數組,而 FastList 是從數組的尾部開始周遊,在 HikariCP 使用的場景下更為高效。
HikariCP 使用清單來儲存打開的 Statement,當 Statement 關閉或 Connection 關閉時需要将對應的 Statement 從清單中移除。通常情況下,同一個Connection建立了多個 Statement 時,後打開的 Statement 會先關閉。是以 FastList在該場景下更加高效。
優化并精簡位元組碼
這裡需要聲明一項誤區,并不是使用位元組碼技術使得代碼性能更好。HikariCP 使用位元組碼技術的目的是減少重複代碼的編輯工作,生成統一的代碼邏輯。但是在這個基礎之上,HikariCP 優化并精簡了生成的位元組碼,提高了性能。
HikariCP 使用 Java 位元組碼修改類庫 Javassist 來生成委托實作動态代理。動态代理的實作在 ProxyFactory 類。Javassist 生成動态代理,是因為其速度更快,相比于 JDK Proxy 生成的位元組碼更少,精簡了很多不必要的位元組碼。
HikariCP 還對項目進行了 JIT 優化。比如說 JIT 方法内聯優化預設的位元組碼個數門檻值為 35 位元組,低于 35 位元組才會進行優化。而 HikariCP 對自己的位元組碼進行研究,精簡了部分方法的位元組碼,使用了諸如減少了類繼承層次結構等方式,将關鍵部分限制在 35 位元組以内,有利于 JIT 進行優化。
比如說 HikariCP 對 invokevirtual 和 invokestatic 兩種位元組碼中函數調用指令的優化。
HikariCP 的早期版本使用單例工廠執行個體來生成 Connection、Statement 和 ResultSet 的代理。該單例工廠執行個體以全局靜态變量 (PROXY_FACTORY) 的形式存在。
public final PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException
{
return PROXY_FACTORY.getProxyPreparedStatement(this, delegate.prepareStatement(sql, columnNames));
}
使用這種方式,編輯出來的位元組碼如下所示 (可以使用 javap 等方式檢視位元組碼)。下邊有詳細的注解,但更加詳細位元組碼的含義還需大家自行學習一下。
public final java.sql.PreparedStatement
prepareStatement(java.lang.String, java.lang.String[]) throws java.sql.SQLException;
flags: ACC_PRIVATE, ACC_FINAL
Code:
stack=5, locals=3, args_size=3
0: getstatic #59 // 擷取靜态變量 PROXY_FACTORY,放入操作數棧
3: aload_0 // 本地變量0中加載值,放入操作數棧,也就是 this
4: aload_0 // 本地變量0中加載值,放入操作數棧,也就是 this
5: getfield #3 // 擷取成員變量 delegate 放入操作數棧,使用操作棧中的 this
8: aload_1 // 将本地變量1放入操作數棧,也就是 sql 變量
9: aload_2 // 将本地變量1放入操作數棧,也就是 columnNames 變量
10: invokeinterface #74, 3 // 調用 prepareStatement 方法
15: invokevirtual #69 // 調用 getProxyPreparedStatement 方法
18: return
通過上邊位元組碼發現,首先要調用 getstatic 指令擷取靜态對象,然後再調用 invokevirtual 指令執行 getProxyPreparedStatement 方法。
HikariCP 後續對此進行了優化,直接使用靜态方法調用,如下所示。getProxyPreparedStatement 方法是 ProxyFactory 靜态方法。
public final PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException
{
return ProxyFactory.getProxyPreparedStatement(this, delegate.prepareStatement(sql, columnNames));
}
這些修改後,位元組碼如下所示。
private final java.sql.PreparedStatement
prepareStatement(java.lang.String, java.lang.String[]) throws java.sql.SQLException;
flags: ACC_PRIVATE, ACC_FINAL
Code:
stack=4, locals=3, args_size=3
0: aload_0
1: aload_0
2: getfield #3 // 擷取 delegate 變量
5: aload_1
6: aload_2
7: invokeinterface #72, 3 // 調用 prepareStatement 方法
12: invokestatic #67 // 調用 getProxyPreparedStatement 靜态方法
15: areturn
這樣修改後不再需要 getstatic 指令,并且使用了 invokestatic 代替 invokevirtual 指令,前者 invokestatic 更容易被JIT優化。另外從堆棧的角度來說,堆棧大小也從原來的 5 變成了 4,方法位元組碼數量也更少了。
ConcurrentBag:更好的并發集合類實作
ConcurrentBag 的實作借鑒于C#中的同名類,是一個專門為連接配接池設計的lock-less集合,實作了比 LinkedBlockingQueue、LinkedTransferQueue 更好的并發性能。
ConcurrentBag 内部同時使用了 ThreadLocal 和 CopyOnWriteArrayList 來存儲元素,其中 CopyOnWriteArrayList 是線程共享的。
ConcurrentBag 采用了 queue-stealing 的機制擷取元素,首先嘗試從 ThreadLocal 中擷取屬于目前線程的元素來避免鎖競争,如果沒有可用元素則再次從共享的 CopyOnWriteArrayList 中擷取。此外,ThreadLocal 和 CopyOnWriteArrayList 在 ConcurrentBag 中都是成員變量,線程間不共享,避免了僞共享 false sharing 的發生。
ConcurrentBag 的具體原理和實作将是下一篇文章的重點内容。
後記
按照文章開始的開源項目研究順序,下一篇文章我們會着重了解 HikariCP 的關鍵特性及其源碼實作,詳細分析它為什麼這麼快,并通過 JMH 實驗資料分析這些優化是如何影響性能的。
個人部落格,歡迎來玩參考
- https://github.com/brettwooldridge/HikariCP/wiki/Down-the-Rabbit-Hole
- https://github.com/brettwooldridge/HikariCP/wiki/Bad-Behavior:-Handling-Database-Down
- http://blog.didispace.com/Springboot-2-0-HikariCP-default-reason/
- https://blog.csdn.net/ClementAD/article/details/46928621
- http://www.timebusker.top/2019/02/15/JAVA%E6%9D%82%E8%AE%B0-Hikaricp%E6%BA%90%E7%A0%81%E8%A7%A3%E8%AF%BB/