三、HikariCP源碼分析之獲取連接流程三

歡迎訪問我的博客,同步更新: 楓山別院
話接上篇,我們繼續(xù)分析HikariCP獲取連接的過程。

③拿到一個(gè)連接

//③
//獲取連接的時(shí)候, 判斷連接是否已經(jīng)被標(biāo)記移除
if (poolEntry.isMarkedEvicted() || (clockSource.elapsedMillis(poolEntry.lastAccessed, now) > ALIVE_BYPASS_WINDOW_MS && !isConnectionAlive(poolEntry.connection))) {
    //如果連接超出maxLifetime, 或者連接測(cè)試不通過, 就關(guān)閉連接
    closeConnection(poolEntry, "(connection is evicted or dead)"); // Throw away the dead connection (passed max age or failed alive test)
    //剩余超時(shí)時(shí)間
    timeout = hardTimeout - clockSource.elapsedMillis(startTime);
} 

如果connectionBag給我們返回了一個(gè)連接,那么需要判斷兩個(gè)條件:

  1. 該連接是否被軟驅(qū)逐了,poolEntry.isMarkedEvicted()

  2. 該連接是否已經(jīng)不可用了或者說已經(jīng)不能通過連接檢查,!isConnectionAlive(poolEntry.connection)

為什么需要判斷呢?連接池里的連接不應(yīng)該都是可用的狀態(tài)嗎?

這里涉及到 HikariCP 的一個(gè)設(shè)計(jì)點(diǎn),HikariCP的連接不是實(shí)時(shí)從連接池里剔除的,只是給連接上打個(gè)標(biāo)記而已,都是在獲取連接的時(shí)候檢查是否可用,如果不可用的時(shí)候才直接從連接池里刪除。如果在 HikariCP的任何地方都可能剔除連接,那么剔除連接的地方會(huì)比較多,會(huì)很亂,也容易引發(fā) bug。反之,把剔除鏈接的操作收縮到某幾個(gè)固定的邏輯中,就比較好管理。

  • 軟驅(qū)逐

我們?cè)谏厦嫣岬揭粋€(gè)軟驅(qū)逐的地方, 就是掛起連接池修改配置的時(shí)候,修改完之后要軟驅(qū)逐所以的連接,使新配置生效。

其實(shí)軟驅(qū)逐是一個(gè)標(biāo)記狀態(tài),是一個(gè)軟刪除,在PoolEntry上,有個(gè)狀態(tài)叫做evict,如果是 true,那么,該連接已經(jīng)被標(biāo)記刪除,不能使用了。然后某個(gè)線程在獲取連接的時(shí)候,正好拿到了這個(gè)連接,判斷出來它已經(jīng)被軟驅(qū)逐,就觸發(fā)從連接池刪除該連接的邏輯。

關(guān)閉連接的邏輯我們后面單獨(dú)分析,此處就不深入了。

  • 連接可用檢查

檢查連接是否可用的條件,其實(shí)是兩個(gè):(clockSource.elapsedMillis(poolEntry.lastAccessed, now) > ALIVE_BYPASS_WINDOW_MS && !isConnectionAlive(poolEntry.connection) 。它們使用 and 連接,也就是這兩個(gè)條件都必須成立。isConnectionAlive方法比較好理解,我們從字面也能看出這個(gè)方法的作用,是判斷連接是否還活著。那么前面的條件是什么呢?

我看其他的解析文章根本沒有提到這里,我們是要解釋一下的。

clockSource.elapsedMillis(poolEntry.lastAccessed, now)這句代碼里,poolEntry.lastAccessed是獲取連接上次使用的時(shí)間,now是當(dāng)前時(shí)間,那么elapsedMillis其實(shí)就是計(jì)算連接到現(xiàn)在多長(zhǎng)時(shí)間沒有被使用過了,結(jié)果是個(gè)毫秒數(shù)。

ALIVE_BYPASS_WINDOW_MS的定義是private final long ALIVE_BYPASS_WINDOW_MS = Long.getLong("com.zaxxer.hikari.aliveBypassWindowMs", MILLISECONDS.toMillis(500));,它看起來像是一個(gè)配置項(xiàng),默認(rèn)值是 500 毫秒。這個(gè)配置你要是從文檔里找的話,是沒有的,因?yàn)檫@個(gè)配置作者沒有透出給用戶使用。但是你要是配置了,是管用的,只是作者不建議用戶修改,所以不透出。它是什么呢?既然跟檢查連接要同時(shí)成立,隨便猜猜也知道跟它有關(guān)。不賣關(guān)子,它是檢查連接是否活著的空窗期,也就是說,如果這個(gè)連接從上次使用到現(xiàn)在,不到 500 毫秒,就不檢查它是否活著了,默認(rèn)它活著;超過 500 毫秒,才檢查一下。

看起來又是一個(gè)優(yōu)化點(diǎn)對(duì)吧?是的,是一個(gè)優(yōu)化點(diǎn)。因?yàn)闄z查連接是否還存活,是比較耗時(shí)的,要使用該連接跟數(shù)據(jù)庫通信一次。

有兩種通信方式:

  1. JDBC4 以下版本的驅(qū)動(dòng),使用用戶配置的connectionTestQuery中的 sql 來檢查。

connectionTestQuery是獲取連接的時(shí)候,用于檢查連接是否可用的一個(gè) sql,大家可能用過,常見的是配置一個(gè)select 1

  1. JDBC4 以上,如果不配置connectionTestQuery, 默認(rèn)使用 ping 命令檢查。

如果使用的是 JDBC4 以上的驅(qū)動(dòng),建議大家不用配置connectionTestQuery,因?yàn)?ping 命令的方式比執(zhí)行一個(gè) sql 要高效很多。

不管是使用較慢的執(zhí)行 sql 檢查還是 較快的ping 命令檢查,這都是一個(gè)耗時(shí)操作,所以作者設(shè)置了一個(gè)空窗期,不需要每次獲取連接都檢查,500毫秒內(nèi)用過該連接,那么連接還正常的可能性極大,就不檢查了,提高性能。

后面closeConnection我們先不說,后面的文章統(tǒng)一分析連接關(guān)閉。

④連接可用

//④
//記錄連接借用
metricsTracker.recordBorrowStats(poolEntry, startTime);
//創(chuàng)建ProxyConnection, ProxyConnection是Connection的包裝, 同時(shí)也創(chuàng)建一個(gè)泄露檢測(cè)的定時(shí)任務(wù)
return poolEntry.createProxyConnection(leakTask.schedule(poolEntry), now);

如果第 3 步的檢查全部通過,也就是拿到的連接是可用的,我們就要執(zhí)行第 4 步了。

  • 上報(bào)監(jiān)控平臺(tái)

metricsTracker這一句,其實(shí)是記錄連接的借用,不是我們通常使用的打印一下日志,而是上報(bào)給監(jiān)控平臺(tái),HikariCP 是支持對(duì)接監(jiān)控平臺(tái)的。這里大家先知道這個(gè)邏輯,后面我們統(tǒng)一分析上報(bào)監(jiān)控平臺(tái)。

  • 為什么用代理連接?

最主要的就是return 的這一句代碼了吧。我們說過poolEntry是底層數(shù)據(jù)庫連接的一個(gè)包裝類,代表一個(gè)數(shù)據(jù)庫連接。那么從createProxyConnection字面來看,這個(gè)方法并不是直接返回?cái)?shù)據(jù)庫連接給用戶使用,而是創(chuàng)建了一個(gè)代理連接,這個(gè)代理連接是什么?為什么不直接返回?cái)?shù)據(jù)庫連接給用戶使用?

不管我們使用 Spring 還是自己寫的代碼從 HikariCP 連接池里拿連接,都是拿到一個(gè)java.sql.Connection類型的對(duì)象沒錯(cuò)吧?它是一個(gè) java 統(tǒng)一的數(shù)據(jù)庫連接接口,不管你使用的是 mysql 還是oracle 等數(shù)據(jù)庫,都是統(tǒng)一對(duì)接這個(gè)接口,都必須返回一個(gè)這個(gè)類型的連接給用戶使用,相當(dāng)于一個(gè)門面模式的設(shè)計(jì),這樣用戶可以不理會(huì)底層使用什么數(shù)據(jù)庫,代碼都是一個(gè)樣的。既然如此,HikariCP應(yīng)該直接返回一個(gè)java.sql.Connection對(duì)吧?

沒有那么簡(jiǎn)單。試想一下,假如 HikariCP 直接返回底層的數(shù)據(jù)庫連接給用戶使用,那么,如果用戶自己關(guān)閉了這個(gè)底層數(shù)據(jù)庫連接呢?那么這個(gè)連接在連接池里相當(dāng)于已經(jīng)不可用了,其他線程也使用不了了。作為一個(gè)框架設(shè)計(jì)者,不能指望每個(gè)用戶都是高手,他們都能在用完數(shù)據(jù)庫連接不會(huì)關(guān)閉它并且要還回連接池中,肯定有小白用戶或者很唬的不管三七二十一的人。更何況除了關(guān)閉連接,還有你修改了連接的設(shè)置呢,比如自動(dòng)提交事務(wù),連接只讀這些設(shè)置,然后沒有恢復(fù)回原來的設(shè)置怎么辦?如此混亂的話,我們使用連接池就沒有意義了。所以我們不能把底層數(shù)據(jù)庫連接直接給用戶使用,這個(gè)大家理解了吧?

如何來實(shí)現(xiàn)呢?我們可以繼承java.sql.Connection,創(chuàng)建一個(gè)它的子類,子類可以直接當(dāng)做父類來用,沒錯(cuò)吧?然后我們?cè)谧宇惱锔采wjava.sql.Connection里面敏感的操作,比如關(guān)閉連接,如果用戶調(diào)用了關(guān)閉連接操作,不是真正的關(guān)閉底層連接,而是將連接還回到連接池。怎么樣?我們解決了用戶瞎用的問題了吧。作者就是這個(gè)目的,才設(shè)計(jì)了一個(gè)createProxyConnection方法來創(chuàng)建了一個(gè)連接的代理ProxyConnection,將這個(gè)代理返回給用戶使用。一切如我們所說的,ProxyConnection繼承了java.sql.Connection,覆蓋了一些方法,詳細(xì)的我們后面單獨(dú)的文章解析,這里很重要。

  • 泄露檢測(cè)

我之前寫過一個(gè)連接泄露檢測(cè)的文章,是我寫的瀏覽量最大的文章,這說明,有不少人都遇到這個(gè)問題。在 HikariCP 檢測(cè)到連接泄露的時(shí)候,會(huì)拋出一個(gè) warn:java.lang.Exception: Apparent connection leak detected。我們?cè)谶@里詳細(xì)說一下這個(gè)地方的邏輯。

  1. 連接泄露檢測(cè)的相關(guān)配置

有一個(gè)leakDetectionThreshold的配置,這個(gè)就是連接泄露檢測(cè)的最大時(shí)間,默認(rèn)是 0,表示不啟用泄露檢測(cè);最小值 2000 毫秒,如果用戶設(shè)置的小于 2000 毫秒,默認(rèn)關(guān)閉泄露檢測(cè),最大值不能超過連接的最大存活時(shí)間,也就是maxLifetime配置,超過的話也會(huì)自動(dòng)禁用泄露檢測(cè)。

  1. 泄露檢測(cè)的定時(shí)任務(wù)

createProxyConnection方法中,我們可以看到傳了一個(gè)參數(shù)leakTask.schedule(poolEntry)leakTask的類型是ProxyLeakTask,它實(shí)現(xiàn)了Runnable接口,是一個(gè)多線程的定時(shí)任務(wù)實(shí)現(xiàn)。它的內(nèi)部持有幾個(gè)成員變量:ScheduledExecutorService,是用來執(zhí)行泄露檢測(cè)定時(shí)任務(wù)的線程池;leakDetectionThreshold,是泄露檢測(cè)超時(shí)時(shí)間;

scheduledFuture是任務(wù)的 future 結(jié)果,可以用來取消定時(shí)任務(wù)。

我們看下它的schedule方法:

ProxyLeakTask schedule(final PoolEntry bagEntry) {
      return (leakDetectionThreshold == 0) ? NO_LEAK : new ProxyLeakTask(this, bagEntry);
   }

這里判斷了下用戶有沒有開啟泄露檢測(cè)功能,如果是沒有開啟,那么就返回一個(gè)NO_LEAK。大家還記得FAUX_LOCK吧?就是上面的①處令牌桶的實(shí)現(xiàn),是提供了一個(gè)空實(shí)現(xiàn)對(duì)吧?這里也是同樣的道理,NO_LEAK是一個(gè)空實(shí)現(xiàn),如果用戶沒有開啟泄露檢測(cè)就方便 JIT 把這段邏輯優(yōu)化掉。

OK,我們看下new ProxyLeakTask(this, bagEntry)的實(shí)現(xiàn):

private ProxyLeakTask(final ProxyLeakTask parent, final PoolEntry poolEntry) {
      this.exception = new Exception("Apparent connection leak detected");
      this.connectionName = poolEntry.connection.toString();
      scheduledFuture = parent.executorService.schedule(this, parent.leakDetectionThreshold, TimeUnit.MILLISECONDS);
}

大家仔細(xì)觀察下這個(gè)構(gòu)造方法,第一個(gè)參數(shù)也是一個(gè)ProxyLeakTask,看名字parent是個(gè)父任務(wù)。這個(gè)父任務(wù)在連接池初始化的時(shí)候會(huì)創(chuàng)建,創(chuàng)建的時(shí)候需要兩個(gè)參數(shù),一個(gè)是用于執(zhí)行任務(wù)的線程池executorService,另一個(gè)是連接泄露超時(shí)時(shí)間leakDetectionThreshold。此處傳遞父任務(wù)進(jìn)來就是要使用父任務(wù)中的線程池和連接泄露超時(shí)時(shí)間。

我們看下超時(shí)檢測(cè)的任務(wù)實(shí)現(xiàn):

public void run() {
      final StackTraceElement[] stackTrace = exception.getStackTrace();
      final StackTraceElement[] trace = new StackTraceElement[stackTrace.length - 5];
      System.arraycopy(stackTrace, 5, trace, 0, trace.length);

      exception.setStackTrace(trace);
      LOGGER.warn("Connection leak detection triggered for {}, stack trace follows", connectionName, exception);
   }

由于這里不太重要,我們就不一句一句的分析了,整個(gè)run方法就是構(gòu)造一個(gè)異常,然后拋出一個(gè) warn 異常棧。

到此,我們整個(gè)連接泄露的分析就結(jié)束了。

  • 釋放鎖

有一個(gè)需要注意的是,我們?cè)谧铋_始的第一句,是申請(qǐng)了一個(gè)令牌,現(xiàn)在上面已經(jīng)獲取到了可用連接,我們需要釋放這個(gè)令牌。我們?cè)谑褂闷渌i的時(shí)候也是一樣的,一定要在最后釋放鎖,為了防止任何異常打斷代碼執(zhí)行,所以釋放鎖的代碼一定要放在 finally 中,保證最后一定會(huì)把鎖釋放掉。

⑤獲取連接超時(shí)

上面整個(gè)獲取連接的過程②③④代碼是放在 do-while 中來執(zhí)行的,只要不超過設(shè)置的connectionTimeout,就會(huì)一直嘗試循環(huán)獲取連接,直到超過了connectionTimeout,就會(huì)執(zhí)行⑤的代碼。超時(shí)之后有兩個(gè)步驟:一是向監(jiān)控平臺(tái)上報(bào)獲取連接超時(shí);二是構(gòu)造一個(gè)異常信息,然后拋出去。

至此,整個(gè)獲取連接的邏輯就介紹完了,可能有一些沒有說到的細(xì)節(jié),大家可以發(fā)表意見,我們一起學(xué)習(xí)討論。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容