SpringBoot HikariCP配置項及源碼解析
前言
在SpringBoot2.0之後,采用的默認數據庫連接池就是Hikari,是一款非常強大,高效,並且號稱“史上最快連接池”。我們知道的連接池有C3P0,DBCP,Druid它們都比較成熟穩定,但性能不是十分好。
我們在日常的編碼中,通常會將一些對象保存起來,這主要考慮的是對象的創建成本;比如像線程資源、數據庫連接資源或者 TCP 連接等,這類對象的初始化通常要花費比較長的時間,如果頻繁地申請和銷毀,就會耗費大量的系統資源,造成不必要的性能損失,於是在Java 中,池化技術應用非常廣泛。在軟件行開發中,軟件的性能是占主導地位的,於是HikariCP就在眾多數據庫連接池中脫穎而出。
為什麼HikariCP性能高
- 優化代理和攔截器:減少代碼。
- 字節碼精簡 :優化代碼(
HikariCP
利用瞭一個第三方的Java字節碼修改類庫Javassist
來生成委托實現動態代理,動態代理的實現在ProxyFactor
y類),直到編譯後的字節碼最少,這樣,CPU
緩存可以加載更多的程序代碼。 - 通過代碼設計和優化大幅減少線程間的鎖競爭。這點主要通過
ConcurrentBag
來實現。 - 自定義數組類型(
FastStatementList
)代替ArrayList:避免每次get()調用都要進行range check,避免調用remove()時的從頭到尾的掃描,相對與ArrayList
極大地提升瞭性能,而其中的區別是,ArrayList
在每次執行get(Index)
方法時,都需要對List
的范圍進行檢查,而FastStatementList
不需要,在能確保范圍的合法性的情況下,可以省去范圍檢查的開銷。自定義集合類型(ConcurrentBag
):支持快速插入和刪除,特別是在同一線程既添加又刪除項時,提高並發讀寫的效率; - 關於
Connection的
操作:另外在Java
代碼中,很多都是在使用完之後直接關閉連接,以前都是從頭到尾遍歷,來關閉對應的Connection
,而HikariCP
則是從尾部對Connection
集合進行掃描,整體上來說,從尾部開始的性能更好一些。 - 針對連接中斷的情況:比其他CP響應時間上有瞭極好的優化,響應時間為5S,會拋出
SqlException
異常,並且後續的getConnection()可以正常進行
下面為大傢附上一張官方的性能測試圖,我們可以從圖上很直觀的看出HikariCP的性能卓越:
常用配置項
autoCommit
控制從池返回的連接的默認自動提交行為,默認為true
connectionTimeout
控制客戶端等待來自池的連接的最大毫秒數。
如果在沒有連接可用的情況下超過此時間,則將拋出 SQLException
。可接受的最低連接超時時間為 250 毫秒
。默認值:30000(30 秒)
idleTimeout
連接允許在池中閑置的最長時間
如果idleTimeout+1秒>maxLifetime
且 maxLifetime>0,則會被重置為0(代表永遠不會退出);如果idleTimeout!=0且小於10秒,則會被重置為10秒
這是HikariCP
用來判斷是否應該從連接池移除空閑連接的一個重要的配置。負責剔除的也還是HouseKeeper
這個定時任務,值為0時,HouseKeeper
不會移除空閑連接,直到到達maxLifetime
後,才會移除,默認值也就是0。
正常情況下,HouseKeeper
會找到所有狀態為空閑的連接隊列,遍歷一遍,將空閑超時到達idleTimeout
且未超過minimumIdle
數量的連接的批量移除。
maxLifetime
池中連接最長生命周期;如果不等於0且小於30秒則會被重置回30分鐘
瞭解這個值的作用前,先瞭解一下MySQLwait_timeout
的作用:MySQL 為瞭防止空閑連接浪費,占用資源,在超過wait_timeout
時間後,會主動關閉該連接,清理資源;默認是28800s
,也就是8小時。簡而言之就是MySQL會在某個連接超過8小時還沒有任何請求時自動斷開連接,但是HikariCP如何知道池子裡的連接有沒有超過這個時間呢?所以就有瞭maxLifetime
,配置後HikariCP會把空閑鏈接超過這個時間的給剔除掉,防止獲取到已經關閉的連接導致異常。
connectionTestQuery
將在從池中向您提供連接之前執行的查詢,以驗證與數據庫的連接是否仍然有效,如select 1
minimumIdle
池中維護的最小空閑連接數;minIdle<0或者minIdle>maxPoolSize,則被重置為maxPoolSize
在HikariCP Pool
創建時,會啟動一個HouseKeeper
定時任務,每隔30s,判斷空閑線程數低於minimumIdle
,並且當前線程池總連接數小於maximumPoolSize
,就建立和MySQL的一個長連接,然後加入到連接池中。官方建議minimumIdle
和maximumPoolSize
保持一致。 因為HikariCP
的HouseKeeper
在發現idleTimeout>0 並且 minimumIdle < maximumPoolSize時,先會去掃描一遍需要移除空閑連接,和MySQL斷開連接。然後再一次性補滿空閑連接數至到minimumIdle
。
maximumPoolSize
池中最大連接數,其實就是線程池中隊列的大小,默認大小為10(包括閑置和使用中的連接)
如果maxPoolSize
小於1,則會被重置。當minIdle<=0被重置為DEFAULT_POOL_SIZE
則為10;如果minIdle>0則重置為minIdle
的值
HikariCP架構
分析源碼之前,先給大傢介紹一下HikariCP的整體架構,整體架構和DBCP2 的有點類似(由此可見 HikariCP 與 DBCP2 性能差異並不是由於架構設計),下面我總結瞭幾點,來和大傢一起探討下:
HikariCP
通過JMX
調用HikariPoolMXBean
來獲取連接池的連接數、獲取等待連接的線程數、丟棄未使用連接、掛起和恢復連接池等。HikariCP
通過JMX
調用HikariConfigMXBean
來動態修改配置。HikariCP
使用HikariConfig
加載配置文件,一般會作為入參來構造 HikariDataSource 對象。HikariPool
是一個非常重要的類,它負責管理連接,涉及到比較多的代碼邏輯。HikariDataSource
主要用於操作HikariPool
獲取連接。ConcurrentBag
用於優化大幅減少線程間的鎖競爭。PoolBase
是HikariPool
的父類,主要負責操作實際的DataSource
獲取連接,並設置連接的一些屬性。
源碼解析
HikariConfig
HikariConfig
保存瞭所有連接池配置,另外實現瞭HikariConfigMXBean
接口,有些配置可以利用JMX
運行時變更。核心配置項屬性會在下面給大傢介紹,這邊Dong哥就簡單介紹一下瞭。
HikariPool
getConnection
public Connection getConnection(final long hardTimeout) throws SQLException { //這裡是防止線程池處於暫停狀態(通常不允許線程池可暫停) suspendResumeLock.acquire(); final long startTime = currentTime(); try { long timeout = hardTimeout; do { //PoolEntry 用於跟蹤connection實例,裡面包裝瞭Connection; //從connectionBag中獲取一個對象,並且檢測是否可用 PoolEntry poolEntry = connectionBag.borrow(timeout, MILLISECONDS); if (poolEntry == null) { break; // We timed out... break and throw exception } final long now = currentTime(); //1、已被標記為驅逐 2、已超過最大存活時間 3、鏈接已死 if (poolEntry.isMarkedEvicted() || (elapsedMillis(poolEntry.lastAccessed, now) > ALIVE_BYPASS_WINDOW_MS && !isConnectionAlive(poolEntry.connection))) { closeConnection(poolEntry, poolEntry.isMarkedEvicted() ? EVICTED_CONNECTION_MESSAGE : DEAD_CONNECTION_MESSAGE); //刷新超時時間 timeout = hardTimeout - elapsedMillis(startTime); } else { metricsTracker.recordBorrowStats(poolEntry, startTime); return poolEntry.createProxyConnection(leakTaskFactory.schedule(poolEntry), now); } //如果沒超時則再次獲取 } while (timeout > 0L); //超時時間到仍未獲取到鏈接則拋出 TimeoutException metricsTracker.recordBorrowTimeoutStats(startTime); throw createTimeoutException(startTime); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new SQLException(poolName + " - Interrupted during connection acquisition", e); } finally { suspendResumeLock.release(); } }
校驗:
- isMarkedEvicted:檢查當前鏈接是否已被驅逐
- elapsedMillis(poolEntry.lastAccessed, now):檢查鏈接是否超過最大存活時間(
maxLifetime
配置時間)
/** * startTime 上次使用時間 * endTime 當前時間 */ static long elapsedMillis(long startTime, long endTime) { return CLOCK.elapsedMillis0(startTime, endTime); }
- isConnectionAlive:連接是否還是存活狀態
boolean isConnectionAlive(final Connection connection) { try { try { //如果支持Connection networkTimeout,則優先使用並設置 setNetworkTimeout(connection, validationTimeout); final int validationSeconds = (int) Math.max(1000L, validationTimeout) / 1000; //如果jdbc實現支持jdbc4 則使用jdbc4 Connection的isValid方法檢測 if (isUseJdbc4Validation) { return connection.isValid(validationSeconds); } //查詢數據庫檢測連接可用性 try (Statement statement = connection.createStatement()) { //如果不支持Connection networkTimeout 則設置Statement queryTimeout if (isNetworkTimeoutSupported != TRUE) { setQueryTimeout(statement, validationSeconds); } statement.execute(config.getConnectionTestQuery()); } } finally { setNetworkTimeout(connection, networkTimeout); if (isIsolateInternalQueries && !isAutoCommit) { connection.rollback(); } } return true; } catch (Exception e) { lastConnectionFailure.set(e); LOGGER.warn("{} - Failed to validate connection {} ({}). Possibly consider using a shorter maxLifetime value.", poolName, connection, e.getMessage()); //捕獲到異常,說明鏈接不可用。(connection is unavailable) return false; } }
HouseKeeper
HouseKeeper
負責保持,我們始終有minimumIdle空閑鏈接可用
private final class HouseKeeper implements Runnable { //默認30s,執行一次 private volatile long previous = plusMillis(currentTime(), -HOUSEKEEPING_PERIOD_MS); @Override public void run() { try { //省略...... String afterPrefix = "Pool "; if (idleTimeout > 0L && config.getMinimumIdle() < config.getMaximumPoolSize()) { logPoolState("Before cleanup "); afterPrefix = "After cleanup "; //空閑鏈接數 final List<PoolEntry> notInUse = connectionBag.values(STATE_NOT_IN_USE); int toRemove = notInUse.size() - config.getMinimumIdle(); for (PoolEntry entry : notInUse) { if (toRemove > 0 && elapsedMillis(entry.lastAccessed, now) > idleTimeout && connectionBag.reserve(entry)) { //關閉過多的空閑超時鏈接 closeConnection(entry, "(connection has passed idleTimeout)"); toRemove--; } } } //記錄pool狀態信息 logPoolState(afterPrefix); //補充空閑鏈接 fillPool(); } catch (Exception e) { LOGGER.error("Unexpected exception in housekeeping task", e); } } }
HouseKeeper
其實是一個線程,也是寫在HikariPool
類裡面的一個內部類,主要負責保持 minimumIdle
的空閑鏈接。HouseKeeper
也用到瞭validationTimeout
, 並且會根據minimumIdle
配置,通過fill 或者 remove保持最少空閑鏈接數。HouseKeeper
線程初始化:
public HikariPool(final HikariConfig config) { super(config); this.connectionBag = new ConcurrentBag<>(this); this.suspendResumeLock = config.isAllowPoolSuspension() ? new SuspendResumeLock() : SuspendResumeLock.FAUX_LOCK; //執行初始化 this.houseKeepingExecutorService = initializeHouseKeepingExecutorService(); //省略...... } }
private ScheduledExecutorService initializeHouseKeepingExecutorService() { if (this.config.getScheduledExecutor() == null) { ThreadFactory threadFactory = (ThreadFactory)Optional.ofNullable(this.config.getThreadFactory()).orElseGet(() -> { return new DefaultThreadFactory(this.poolName + " housekeeper", true); }); //ScheduledThreadPoolExecutor是ThreadPoolExecutor類的子類,Java推薦僅在開發定時任務程序時采用ScheduledThreadPoolExecutor類 ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1, threadFactory, new DiscardPolicy()); //傳入false,則執行shutdown()方法之後,待處理的任務將不會被執行 executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); //取消任務後,判斷是否需要從阻塞隊列中移除任務 executor.setRemoveOnCancelPolicy(true); return executor; } else { return this.config.getScheduledExecutor(); } }
HikariDataSource
HikariDataSource
非常重要,主要用於操作HikariPool
獲取連接,並且能夠清除空閑連接。
public class HikariDataSource extends HikariConfig implements DataSource, Closeable { private final AtomicBoolean isShutdown = new AtomicBoolean(); //final修飾,構造時決定,如果使用無參構造為null,使用有參構造和pool一樣 private final HikariPool fastPathPool; //volatile修飾,無參構造不會設置pool,在getConnection時構造pool,有參構造和fastPathPool一樣。 private volatile HikariPool pool; public HikariDataSource() { super(); fastPathPool = null; } public HikariDataSource(HikariConfig configuration) { configuration.validate(); configuration.copyStateTo(this); pool = fastPathPool = new HikariPool(this); this.seal(); } }
以上就是SpringBoot HikariCP配置項及源碼解析的詳細內容,更多關於SpringBoot HikariCP配置的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- 詳解MySQL連接掛死的原因
- SpringBoot默認使用HikariDataSource數據源方式
- SpringBoot2.0.3打印默認數據源為 HikariDataSource (null)問題
- Sharding-JDBC自動實現MySQL讀寫分離的示例代碼
- spring boot中配置hikari連接池屬性方式