01Hikari 源碼解析之connection相關
一、初始化
類 HikariDataSource 的 getConnection() 爲 取connection 的方法,主要流程如下:
- 判斷 當前的 dataSource 是否關閉,如果關閉 ,拋出異常
- 判斷fastPathPool 是否爲null ,默認爲null
- 判斷當前的pool 是否爲null ,如果爲空,那就就行初始化,所以項目啓動完了之後,第一次請求時間比較長
- 不爲空 ,返回 result.getConnection()
public Connection getConnection() throws SQLException{
if (isClosed()) {
throw new SQLException("HikariDataSource " + this + " has been closed.");
}
if (fastPathPool != null) {
return fastPathPool.getConnection();
}
// See http://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java
HikariPool result = pool;
if (result == null) {
synchronized (this) {
result = pool;
... ... ...... 省略,詳細過程在下面分析
xxxx .... xxxxx
}
}
}
第一次獲取connection的時候,需要初始化,大致流程如下:
- 設置poolName ,前綴 HikariPool- ,後面跟的是 pool_number
- validateNumerics,這裏是 對一些參數校驗,具體 查看下面的表格
- 對 pool 進行初始,裏面還有一系列的 參數初始化 ,這裏暫不展開,主要涉及houseKeeperTask ,這個是 在delay 100ms 之後 每隔 30秒 定時運行,詳細在下面概述
1.1 houseKeeperTask 定時清理 和 填充連接池
在 初始化的時候, houseKeeperTask 設置爲 initialDelay = 100L, 每隔30S(默認) 運行一次
this.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, housekeepingPeriodMs, MILLISECONDS);
主要邏輯在 內部類 HouseKeeper的run() 方法裏面,看一下主要的邏輯,就是不斷的維護線程池裏面的連接數
public void run() {
try {
... 省略 ...
previous = now;
String afterPrefix = "Pool ";
// 如果idleTimeout >0 ,默認10分鐘,600000ms 並且 最小連接數 小於最大連接數
// 如果不配置 最小連接數 ,最小連接數 默認和 max_num_size 一樣大,都是默認10
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--;
}
}
}
logPoolState(afterPrefix);
//進行連接池補充
fillPool(); // Try to maintain minimum connections
}
catch (Exception e) {
LOGGER.error("Unexpected exception in housekeeping task", e);
}
}
private synchronized void fillPool()
{
final int connectionsToAdd = Math.min(config.getMaximumPoolSize() - getTotalConnections(), config.getMinimumIdle() - getIdleConnections())
- addConnectionQueue.size();
for (int i = 0; i < connectionsToAdd; i++) {
addConnectionExecutor.submit((i < connectionsToAdd - 1) ? POOL_ENTRY_CREATOR : POST_FILL_POOL_ENTRY_CREATOR);
}
}
1.2 創建連接 createPoolEntry
這裏主要介紹一下 createPoolEntry 方法,重點就是 創建了一個 poolEntry之後,在到 maxlifetime 最大還差2.5% 時,進行標記,如果 當前空閒直接回收,如果回收成功,在判斷一下是否需要新建一個poolEntry
private PoolEntry createPoolEntry()
{
try {
final PoolEntry poolEntry = newPoolEntry();
final long maxLifetime = config.getMaxLifetime();
if (maxLifetime > 0) {
// variance up to 2.5% of the maxlifetime
final long variance = maxLifetime > 10_000 ? ThreadLocalRandom.current().nextLong( maxLifetime / 40 ) : 0;
final long lifetime = maxLifetime - variance;
// 快接近最大存活時間時,對連接進行標記,如果當前連接空閒,直接回收
如果回收成功,再判斷是否有等待線程,如果有說明當前資源緊張,再創建一個poolEntry
poolEntry.setFutureEol(houseKeepingExecutorService.schedule(
() -> {
if (softEvictConnection(poolEntry, "(connection has passed maxLifetime)", false /* not owner */)) {
addBagItem(connectionBag.getWaitingThreadCount());
}
},
lifetime, MILLISECONDS));
}
return poolEntry;
}
catch (Exception e) {
if (poolState == POOL_NORMAL) { // we check POOL_NORMAL to avoid a flood of messages if shutdown() is running concurrently
LOGGER.debug("{} - Cannot acquire connection from data source", poolName, (e instanceof ConnectionSetupException ? e.getCause() : e));
}
return null;
}
}
public void addBagItem(final int waiting)
{
final boolean shouldAdd = waiting - addConnectionQueue.size() >= 0; // Yes, >= is intentional.
if (shouldAdd) {
addConnectionExecutor.submit(POOL_ENTRY_CREATOR);
}
}
二、獲取Connection
上面已經大致涉及第一次初始化的 過程,接下來概述一些 getConnection的流程,主要是在類HikariPool 的
getConnection() 裏面,代碼如下,
public Connection getConnection(final long hardTimeout) throws SQLException
{
// 獲取信號量,最大值1000
suspendResumeLock.acquire();
final long startTime = currentTime();
try {
long timeout = hardTimeout;
do {
// 從connectionBag 裏面 獲取borrow
PoolEntry poolEntry = connectionBag.borrow(timeout, MILLISECONDS);
if (poolEntry == null) {
break; // We timed out... break and throw exception
}
final long now = currentTime();
// 判斷當前的poolEntry 是否有效,無效就關閉
if (poolEntry.isMarkedEvicted() || (elapsedMillis(poolEntry.lastAccessed, now) > aliveBypassWindowMs && !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); // 在connectionTimeout 時間內 一直循環直到找到可用的
metricsTracker.recordBorrowTimeoutStats(startTime);
throw createTimeoutException(startTime);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new SQLException(poolName + " - Interrupted during connection acquisition", e);
}
finally {
suspendResumeLock.release();
}
}
其流程圖如下:
大致流程如下:
-
獲取信號量suspendResumeLock ,這裏 默認是 1000個
-
從connectionBag 裏面獲取 poolEntry ,這裏 做了一定的優化 ,ConcurrentBag(borrow)詳細的見02Hikari源碼解析之ConcurrentBag、FastList分析 ,這裏概述一下:
2.1. 先從 threadList 裏面獲取,同一個Request 即同一個線程時 從裏面獲取
2.2. 如果threadList 裏面可用的 ,再從sharedList 裏面獲取 ,sharedList 存放ConcurrentBag中全部用於出借的資源 ,這裏有一個注意點,這裏也是 hkari 快的原因,就是判斷如果 還有其他的等待者,趕緊的再去創建連接,而沒有判斷 當前 sharedList 包裏面 是否夠用了,所以 高QPS的情況下,很容易連接池達到最大的量
2.3. 當現有全部資源全部在使用中,等待一個被釋放的資源或者一個新資源,handoffQueue裏面獲取 -
判斷 poolEntity 是否被標記驅逐 或者 是否connectionAlive 以及上一次訪問時間到當前是否 大於 aliveBypassWindowMs(默認 500ms)
-
如果是有效連接,便開始 創建createProxyConnection
-
否則 繼續循環 2-3 的 步驟,直到 connectionTimeout 用完
三、歸還Connection
邏輯代碼主要在 HikariPool 類下面的 recycle(final PoolEntry poolEntry) ,流程圖如下:
主要邏輯:
1.先將裏面的所有Statement 關閉,並將保存的list 對象openStatements 清空
2.將 代理delegate 狀態設置爲 CLOSE_CONNECTION
3.將 poolEntity 的狀態設置爲 STATE_NOT_IN_USE
4.判斷是否有 等待線程,如果有 直接推到 handoffQueue 隊列 進行第一時間的交換
5.否則放入 本地 threadLocalList ,默認是弱引用,這樣不影響回收
四、總結
Hikari 在獲取連接的時候,做了一定的優化:
首先 查看 是否有可用的連接,從 ThreadLocal<List> 裏面獲取,這裏 的 list ,是自定義了一個 FastList,而 FastList 和普通的ArrayList 不同之處就是 Fast 在get() 和remove () 方面去掉了rangeCheck(index); 一定程度上又快了一些.
全部資源用了 CopyOnWriteArrayList 定義的 sharedList ,這樣讀的是沒有鎖,寫的時候進行了 加鎖操作
SynchronousQueue 是使用了 公平鎖 機制, SynchronousQueue 就是一定程度上無緩存的隊列.
Hikari 作者 不推薦使用minimumIdle,該屬性控制HikariCP嘗試在池中維護的最小空閒連接數。如果空閒連接低於此值並且池中的總連接數少於maximumPoolSize,HikariCP將盡最大努力快速高效地添加其他連接。但是,爲了獲得最佳性能和響應尖峯需求,我們建議不要設置此值,而是允許HikariCP充當固定大小的連接池。
Hikari 作者 認爲如果minimumIdle小於maximumPoolSize的話,在流量激增的時候需要額外的連接,此時在請求方法裏頭再去處理新建連接會造成性能損失,即會導致數據庫一方面降低連接建立的速度,另一方面也會影響既有的連接事務的完成,間接影響了這些既有連接歸還到連接池的速度。 Hikari 作者 認爲minimumIdle與maximumPoolSize設置成一樣,多餘的空閒連接不會對整體的性能有什麼嚴重影響。