Spring Boot整合資料庫連線池 HikariCP
摘要:淺談連線池基本概念和工作原理、常見資料庫連線池效能對比、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連線資料庫用到了懶載入。