Java 數據持久化系列之 HikariCP (一)
- 2020 年 4 月 14 日
- 筆記
在上一篇《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 在日語中的含義是光,作者特意用這個含義來表示這塊資料庫連接池真的速度很快。官方地址是 //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 實驗數據分析這些優化是如何影響性能的。
參考
- //github.com/brettwooldridge/HikariCP/wiki/Down-the-Rabbit-Hole
- //github.com/brettwooldridge/HikariCP/wiki/Bad-Behavior:-Handling-Database-Down
- //blog.didispace.com/Springboot-2-0-HikariCP-default-reason/
- //blog.csdn.net/ClementAD/article/details/46928621
- //www.timebusker.top/2019/02/15/JAVA雜記-Hikaricp源碼解讀/