摘要:淺談連接池基本概念和工作原理、常見數據庫連接池性能對比、HiKariCP速度為什么快和常見屬性對比。最后給出一個Spring Boot整合HiKariCP的入門案例。
§工程環境
- JDK:1.8.0_231
- maven:3.6.1
- Apache Tomcat:9.0.46
- Spring Boot: 2.5.0
- mysql-connector-java:8.0.25
- mysql:8.0.25
- HikariCP:4.0.3
§數據庫連接池介紹
一個普通的java程序,要查詢數據庫的數據,基本流程是這樣的:

可以看到:進行一次查詢,要進行很多次網絡交互,這樣的缺點是:
-
網絡IO多
-
響應時間長,導致QPS降低
-
頻繁創建連接和關閉連接,浪費數據庫資源,影響服務器性能
因為TCP連接的創建開支十分昂貴,並且數據庫所能承載的TCP並發連接數也有限制,針對這種場景,數據庫連接池應運而生。數據庫連接池是用於創建、管理和釋放數據庫連接的緩沖池技術,緩沖池中的連接可以被任何需要它們的線程使用。當一個線程需要用JDBC對一個數據庫操作時,將從池中請求一個連接;當這個連接使用完畢后,將返回到連接池中,等待其它線程的調度。
這里用到了池化技術,如大家屢見不鮮的線程池、整數池、字符串池、對象池和Http 連接池等等,都是對這個思想的應用。池化技術的思想主要是通過復用對象,以減少每次獲取資源時創建和釋放所帶來的資源消耗,提高資源利用率,這是典型的以空間換取時間的策略。
數據庫連接池負責分配、管理、釋放數據庫連接,它允許應用服務重復使用數據庫連接,而非重新建立。使用連接池之后,流程是這樣的:

由此可見,數據庫連接的創建和關閉連接均由連接池來實現。這樣的機制有如下兩個優點:
- 封裝關於數據庫訪問的各種參數,實現統一管理
- 通過對數據庫的連接池管理,減少網絡開銷並提升數據庫性能
數據庫連接池工作原理剖析
數據庫連接池的工作原理主要由三部分組成,分別為
- 連接池的建立
- 連接池的管理
- 連接池的關閉
-
連接池的建立。應用初始化時,根據配置的最小連接數,在連接池將創建此數目的數據庫連接放到池中,以便使用時能從連接池中獲取。連接池中的連接不能隨意創建和關閉,這樣避免了連接隨意建立和關閉造成的系統開銷。
Java中提供了很多容器類可以方便的構建連接池,例如Vector、Stack等。

-
連接池的管理。連接池管理策略是連接池機制的核心,連接池內連接的分配和釋放對系統的性能有很大的影響。其管理策略是:當客戶請求數據庫連接時,首先查看連接池中是否有空閑連接,如果存在空閑連接,則將連接分配給客戶使用;如果沒有空閑連接,則查看當前所開的連接數是否已經達到最大連接數,如果沒達到就重新創建一個連接給請求的客戶;如果達到就按設定的最大等待時間進行等待,如果超出最大等待時間,則拋出異常給客戶。 當客戶釋放數據庫連接時,先判斷池中的連接數是否超過了設置的最大連接數,如果超過就從連接池中刪除該連接;否則,保留連接,等待再次使用。

-
連接池的關閉。應用程序關閉時,關閉連接池中所有連接,釋放所有相關資源。

在Java這個自由開放的生態中,已經有非常多優秀的開源數據庫連接池可以供大家選擇,比如:DBCP、C3P0、Druid、HikariCP、tomcat-jdbc等。而在Spring Boot 2.x中,對數據源的選擇也緊跟潮流,采用了目前性能最佳的HikariCP。接下來,我們就來具體聊聊HikariCP。
§Java常見數據庫連接池性能比較
單從性能角度分析,性能從高到低依次是:HikariCP、druid、tomcat-jdbc、dbcp、c3p0。下圖是HikariCP官網給出的性能對比:

從上圖中可以直觀的看出,Hikari 在 獲取和釋放 Connection 和 Statement 方法的 OPS 不是一般的高,那是相當的高,基本上是碾壓其他連接池,這里就不一一點名了。除了 OPS 外,HikariCP 的穩定性也更好,性能毛刺更少。

§數據庫連接池選型 Druid vs HikariCP性能對比
- 從功能角度考慮,Druid 功能更豐富,除具備連接池基本功能外,還支持sql級監控、擴展、SQL防注入等。最新版甚至有集群監控。兩者的側重點不一樣。
- 從性能角度考慮,從數據處理速度角度來看,HikariCP確實更強,但Druid由阿里巴巴背書,可支持”雙十一”等最嚴苛的使用場景,並且提供了強大的監控功能,在國內有不少用戶。不過,Spring Boot 2.x已經使用HikariCP作為默認的數據庫連接池,其優秀程度可見一斑。
- 從監控角度考慮,如果我們有像skywalking、prometheus等組件是可以將監控能力交給這些的,HikariCP也可以將metrics暴露出去。
HikariCP作為后起之秀,是目前最快的Java數據庫連接池。
§HikariCP為什么這么快
HikariCP為什么這么快呢?是因為它在如下四個方面做了優化,以提升性能:
- 優化並精簡字節碼。使用Java字節碼修改類庫Javassist來生成委托實現動態代理,比JDK Proxy生成的字節碼更少,精簡了很多不必要的字節碼。
- 使用自定義的無鎖的、性能更好的並發集合類ConcurrentBag。
- 使用自定義的數組類型FastList替代ArrayList。FastList是List接口的精簡實現。
- 優化代理和攔截器:減少代碼,例如 HikariCP 的 Statement proxy 只有100行代碼,只有 BoneCP 的十分之一;
下面是FastList源碼:
/**
* ArrayList精簡版的、沒有列表檢查的 FastList
*
* @author Brett Wooldridge
*/
public final class FastList<T> implements List<T>, RandomAccess, Serializable{
private static final long serialVersionUID = -4598088075242913858L;
private final Class<?> clazz;
private T[] elementData;
private int size;
/**
* 構建一個默認大小為32的列表。
* @param clazz the Class stored in the collection
*/
@SuppressWarnings("unchecked")
public FastList(Class<?> clazz) {
this.elementData = (T[]) Array.newInstance(clazz, 32);
this.clazz = clazz;
}
/**
* 構造具有指定大小的列表。
* @param clazz the Class stored in the collection
* @param capacity the initial size of the FastList
*/
@SuppressWarnings("unchecked")
public FastList(Class<?> clazz, int capacity) {
this.elementData = (T[]) Array.newInstance(clazz, capacity);
this.clazz = clazz;
}
@Override
public boolean add(T element) {
//給 list添加屬性
//如果 size值小於 初始化的值
if (size < elementData.length) {
elementData[size++] = element;
} else {
// 溢出的代碼
//elementData 原始32不夠用 需要擴容
final int oldCapacity = elementData.length;
final int newCapacity = oldCapacity << 1;
@SuppressWarnings("unchecked")
//擴容集合
final T[] newElementData = (T[]) Array.newInstance(clazz, newCapacity);
//數組復制
System.arraycopy(elementData, 0, newElementData, 0, oldCapacity);
//屬性賦值
newElementData[size++] = element;
elementData = newElementData;
}
return true;
}
/**
* 貼出ArrayList的get代碼,來看看為什么 FastList 更快
* public E get(int index) {
* rangeCheck(index);
* return elementData(index);
* }
* ArrayList調用rangeCheck以檢查角標范圍,而FastList直接讀取元素,節約時間
*/
@Override
public T get(int index) {
return elementData[index];
}
/**
* 這個是ArrayList的 remove()代碼, FastList 少了檢查范圍和從頭到尾的 檢查元素動作,速度更快
* rangeCheck(index);
* modCount++;
* E oldValue = elementData(index);
*/
@Override
public boolean remove(Object element) {
for (int index = size - 1; index >= 0; index--) {
if (element == elementData[index]) {
final int numMoved = size - index - 1;
//如果角標不是最后一個 copy一個新的數組結構
if (numMoved > 0) {
System.arraycopy(elementData, index + 1, elementData, index, numMoved);
}
//如果角標是最后面的 直接初始化為null
elementData[--size] = null;
return true;
}
}
return false;
}
§數據源配置詳解
由於Spring Boot的自動化配置機制,大部分對於數據源的配置都可以通過配置參數的方式去改變。只有一些特殊情況,比如:更換默認數據源,多數據源共存等情況才需要去修改覆蓋初始化的Bean內容。本節我們主要講Hikari的配置,所以對於使用其他數據源或者多數據源的情況,在之后的教程中學習。
在Spring Boot自動化配置中,對於數據源的配置可以分為兩類:
- 通用配置:以
spring.datasource.*的形式存在,主要是對一些即使使用不同數據源也都需要配置的一些常規內容。比如:數據庫鏈接地址、用戶名、密碼等。這里就不做過多說明了,通常就這些配置:
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
溫馨提示:driver-class-name用於指定JDBC驅動程序的類名,默認從jdbc url中自動探測。
com.mysql.jdbc.Driver 是 mysql-connector-java 5中的,com.mysql.cj.jdbc.Driver 是 mysql-connector-java 版本6以后的。
- 數據源連接池配置:以
spring.datasource.<數據源名稱>.*的形式存在,比如:Hikari的配置參數就是spring.datasource.hikari.*形式。下面這個是我們最常用的幾個配置項及對應說明:
spring.datasource.hikari.minimum-idle=10
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.idle-timeout=500000
spring.datasource.hikari.max-lifetime=540000
spring.datasource.hikari.connection-timeout=60000
spring.datasource.hikari.connection-test-query=SELECT 1
這些配置的含義:
-
spring.datasource.hikari.minimum-idle: 最小空閑連接,默認值10,小於0或大於maximum-pool-size,都會重置為maximum-pool-size。 -
spring.datasource.hikari.maximum-pool-size: 最大連接數,小於等於0會被重置為默認值10;大於零小於1會被重置為minimum-idle的值 -
spring.datasource.hikari.idle-timeout: 空閑連接超時時間,此屬性控制允許連接在連接池中閑置的最長時間。默認值600000毫秒(10分鍾),大於等於max-lifetime且max-lifetime>0,會被重置為0;不等於0且小於10秒,會被重置為10秒。此設置僅適用於maximumPoolSize-minimumIdle的連接。一旦連接池達到最小連接數,空閑連接將不會退出。在超時之前,連接永遠不會退出。值為0意味着空閑連接永遠不會從池中刪除。允許的最小值是10000ms(10秒),默認值值是600000(10分鍾)。
-
spring.datasource.hikari.max-lifetime: 連接最大存活時間,不等於0且小於30秒,會被重置為默認值30分鍾.設置應該比mysql設置的超時時間短 -
spring.datasource.hikari.connection-timeout: 連接超時時間:毫秒,小於250ms,否則被重置為默認值30秒 -
spring.datasource.hikari.connection-test-query: 用於測試連接是否可用的查詢語句
更多完整配置項可查看下表:
| name | 默認配置validate之后的值 | 構造器默認值 | validate重置 | 描述 |
|---|---|---|---|---|
| autoCommit | TRUE | TRUE | – | 自動提交從池中返回的連接 |
| connectionTimeout | 30000 | SECONDS.toMillis(30) = 30000 | 如果小於250毫秒,則被重置回30秒 | 等待來自池的連接的最大毫秒數 |
| idleTimeout | 600000 | MINUTES.toMillis(10) = 600000 | 如果idleTimeout+1秒>maxLifetime 且 maxLifetime>0,則會被重置為0(代表永遠不會退出);如果idleTimeout!=0且小於10秒,則會被重置為10秒 | 連接允許在池中閑置的最長時間 |
| maxLifetime | 1800000 | MINUTES.toMillis(30) = 1800000 | 如果不等於0且小於30秒則會被重置回30分鍾 | 池中連接最長生命周期 |
| connectionTestQuery | null | null | – | 如果您的驅動程序支持JDBC4,我們強烈建議您不要設置此屬性 |
| minimumIdle | 10 | -1 | minIdle<0或者minIdle>maxPoolSize,則被重置為maxPoolSize | 池中維護的最小空閑連接數 |
| maximumPoolSize | 10 | -1 | 如果maxPoolSize小於1,則會被重置。當minIdle<=0被重置為DEFAULT_POOL_SIZE則為10;如果minIdle>0則重置為minIdle的值 | 池中最大連接數,包括閑置和使用中的連接 |
| metricRegistry | null | null | – | 該屬性允許您指定一個 Codahale / Dropwizard MetricRegistry 的實例,供池使用以記錄各種指標 |
| healthCheckRegistry | null | null | – | 該屬性允許您指定池使用的Codahale / Dropwizard HealthCheckRegistry的實例來報告當前健康信息 |
| poolName | HikariPool-1 | null | – | 連接池的用戶定義名稱,主要出現在日志記錄和JMX管理控制台中以識別池和池配置 |
| initializationFailTimeout | 1 | 1 | – | 如果池無法成功初始化連接,則此屬性控制池是否將 fail fast |
| isolateInternalQueries | FALSE | FALSE | – | 是否在其自己的事務中隔離內部池查詢,例如連接活動測試 |
| allowPoolSuspension | FALSE | FALSE | – | 控制池是否可以通過JMX暫停和恢復 |
| readOnly | FALSE | FALSE | – | 從池中獲取的連接是否默認處於只讀模式 |
| registerMbeans | FALSE | FALSE | – | 是否注冊JMX管理Bean(MBeans) |
| catalog | null | driver default | – | 為支持 catalog 概念的數據庫設置默認 catalog |
| connectionInitSql | null | null | – | 該屬性設置一個SQL語句,在將每個新連接創建后,將其添加到池中之前執行該語句。 |
| driverClassName | null | null | – | HikariCP將嘗試通過僅基於jdbcUrl的DriverManager解析驅動程序,但對於一些較舊的驅動程序,還必須指定driverClassName |
| transactionIsolation | null | null | – | 控制從池返回的連接的默認事務隔離級別 |
| validationTimeout | 5000 | SECONDS.toMillis(5) = 5000 | 如果小於250毫秒,則會被重置回5秒 | 連接將被測試活動的最大時間量 |
| leakDetectionThreshold | 0 | 0 | 如果大於0且不是單元測試,則進一步判斷:(leakDetectionThreshold < SECONDS.toMillis(2) or (leakDetectionThreshold > maxLifetime && maxLifetime > 0),會被重置為0 . 即如果要生效則必須>0,而且不能小於2秒,而且當maxLifetime > 0時不能大於maxLifetime | 記錄消息之前連接可能離開池的時間量,表示可能的連接泄漏 |
| dataSource | null | null | – | 這個屬性允許你直接設置數據源的實例被池包裝,而不是讓HikariCP通過反射來構造它 |
| schema | null | driver default | – | 該屬性為支持模式概念的數據庫設置默認模式 |
| threadFactory | null | null | – | 此屬性允許您設置將用於創建池使用的所有線程的java.util.concurrent.ThreadFactory的實例。 |
| scheduledExecutor | null | null | – | 此屬性允許您設置將用於各種內部計划任務的java.util.concurrent.ScheduledExecutorService實例 |
§數據源配置案例
數據庫連接池properties文件配置信息:
###數據源配置###
#默認就是hikari,可缺省
#spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mysql?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true
spring.datasource.username=root
spring.datasource.password=root
#默認30000ms,即30s
#spring.datasource.hikari.connection-timeout=30000
#存活時間,默認600000ms,即10min
spring.datasource.hikari.idle-timeout=600000
#連接池的最大尺寸(閑置連接+正在使用的連接),默認10
spring.datasource.hikari.maximum-pool-size=200
#最小空閑連接數,默認10
spring.datasource.hikari.minimum-idle=50
spring.datasource.hikari.pool-name=私有連接池
HikariDataSource在應用啟動后,第一次數據庫交互的時候加載連接池信息,這就是因為Spring Boot 2.x連接數據庫用到了懶加載。
