歡迎訪問我的博客鸯绿,同步更新: 楓山別院
話接上篇,我們繼續(xù)分析HikariCP獲取連接的過程昂儒。
③拿到一個連接
//③
//獲取連接的時候, 判斷連接是否已經(jīng)被標(biāo)記移除
if (poolEntry.isMarkedEvicted() || (clockSource.elapsedMillis(poolEntry.lastAccessed, now) > ALIVE_BYPASS_WINDOW_MS && !isConnectionAlive(poolEntry.connection))) {
//如果連接超出maxLifetime, 或者連接測試不通過, 就關(guān)閉連接
closeConnection(poolEntry, "(connection is evicted or dead)"); // Throw away the dead connection (passed max age or failed alive test)
//剩余超時時間
timeout = hardTimeout - clockSource.elapsedMillis(startTime);
}
如果connectionBag
給我們返回了一個連接沟使,那么需要判斷兩個條件:
該連接是否被軟驅(qū)逐了,
poolEntry.isMarkedEvicted()
該連接是否已經(jīng)不可用了或者說已經(jīng)不能通過連接檢查渊跋,
!isConnectionAlive(poolEntry.connection)
為什么需要判斷呢格带?連接池里的連接不應(yīng)該都是可用的狀態(tài)嗎?
這里涉及到 HikariCP 的一個設(shè)計點(diǎn)刹枉,HikariCP的連接不是實(shí)時從連接池里剔除的,只是給連接上打個標(biāo)記而已屈呕,都是在獲取連接的時候檢查是否可用微宝,如果不可用的時候才直接從連接池里刪除。如果在 HikariCP的任何地方都可能剔除連接虎眨,那么剔除連接的地方會比較多蟋软,會很亂镶摘,也容易引發(fā) bug。反之岳守,把剔除鏈接的操作收縮到某幾個固定的邏輯中凄敢,就比較好管理。
- 軟驅(qū)逐
我們在上面提到一個軟驅(qū)逐的地方湿痢, 就是掛起連接池修改配置的時候涝缝,修改完之后要軟驅(qū)逐所以的連接,使新配置生效譬重。
其實(shí)軟驅(qū)逐是一個標(biāo)記狀態(tài)拒逮,是一個軟刪除,在PoolEntry
上臀规,有個狀態(tài)叫做evict
滩援,如果是 true,那么塔嬉,該連接已經(jīng)被標(biāo)記刪除玩徊,不能使用了。然后某個線程在獲取連接的時候谨究,正好拿到了這個連接恩袱,判斷出來它已經(jīng)被軟驅(qū)逐,就觸發(fā)從連接池刪除該連接的邏輯记盒。
關(guān)閉連接的邏輯我們后面單獨(dú)分析憎蛤,此處就不深入了。
- 連接可用檢查
檢查連接是否可用的條件纪吮,其實(shí)是兩個:(clockSource.elapsedMillis(poolEntry.lastAccessed, now) > ALIVE_BYPASS_WINDOW_MS && !isConnectionAlive(poolEntry.connection)
俩檬。它們使用 and 連接,也就是這兩個條件都必須成立碾盟。isConnectionAlive
方法比較好理解棚辽,我們從字面也能看出這個方法的作用,是判斷連接是否還活著冰肴。那么前面的條件是什么呢屈藐?
我看其他的解析文章根本沒有提到這里,我們是要解釋一下的熙尉。
clockSource.elapsedMillis(poolEntry.lastAccessed, now)
這句代碼里联逻,poolEntry.lastAccessed
是獲取連接上次使用的時間,now
是當(dāng)前時間检痰,那么elapsedMillis
其實(shí)就是計算連接到現(xiàn)在多長時間沒有被使用過了包归,結(jié)果是個毫秒數(shù)。
ALIVE_BYPASS_WINDOW_MS
的定義是private final long ALIVE_BYPASS_WINDOW_MS = Long.getLong("com.zaxxer.hikari.aliveBypassWindowMs", MILLISECONDS.toMillis(500));
铅歼,它看起來像是一個配置項(xiàng)公壤,默認(rèn)值是 500 毫秒换可。這個配置你要是從文檔里找的話,是沒有的厦幅,因?yàn)檫@個配置作者沒有透出給用戶使用沾鳄。但是你要是配置了,是管用的确憨,只是作者不建議用戶修改译荞,所以不透出。它是什么呢缚态?既然跟檢查連接要同時成立磁椒,隨便猜猜也知道跟它有關(guān)。不賣關(guān)子玫芦,它是檢查連接是否活著的空窗期浆熔,也就是說,如果這個連接從上次使用到現(xiàn)在桥帆,不到 500 毫秒医增,就不檢查它是否活著了,默認(rèn)它活著老虫;超過 500 毫秒叶骨,才檢查一下。
看起來又是一個優(yōu)化點(diǎn)對吧祈匙?是的忽刽,是一個優(yōu)化點(diǎn)。因?yàn)闄z查連接是否還存活夺欲,是比較耗時的跪帝,要使用該連接跟數(shù)據(jù)庫通信一次。
有兩種通信方式:
- JDBC4 以下版本的驅(qū)動些阅,使用用戶配置的
connectionTestQuery
中的 sql 來檢查伞剑。
connectionTestQuery
是獲取連接的時候,用于檢查連接是否可用的一個 sql市埋,大家可能用過黎泣,常見的是配置一個select 1
。
- JDBC4 以上缤谎,如果不配置
connectionTestQuery
抒倚, 默認(rèn)使用 ping 命令檢查。
如果使用的是 JDBC4 以上的驅(qū)動坷澡,建議大家不用配置connectionTestQuery
衡便,因?yàn)?ping 命令的方式比執(zhí)行一個 sql 要高效很多。
不管是使用較慢的執(zhí)行 sql 檢查還是 較快的ping 命令檢查,這都是一個耗時操作镣陕,所以作者設(shè)置了一個空窗期,不需要每次獲取連接都檢查姻政,500毫秒內(nèi)用過該連接呆抑,那么連接還正常的可能性極大,就不檢查了汁展,提高性能鹊碍。
后面closeConnection
我們先不說,后面的文章統(tǒng)一分析連接關(guān)閉食绿。
④連接可用
//④
//記錄連接借用
metricsTracker.recordBorrowStats(poolEntry, startTime);
//創(chuàng)建ProxyConnection, ProxyConnection是Connection的包裝, 同時也創(chuàng)建一個泄露檢測的定時任務(wù)
return poolEntry.createProxyConnection(leakTask.schedule(poolEntry), now);
如果第 3 步的檢查全部通過侈咕,也就是拿到的連接是可用的,我們就要執(zhí)行第 4 步了器紧。
- 上報監(jiān)控平臺
metricsTracker
這一句耀销,其實(shí)是記錄連接的借用,不是我們通常使用的打印一下日志铲汪,而是上報給監(jiān)控平臺熊尉,HikariCP 是支持對接監(jiān)控平臺的。這里大家先知道這個邏輯掌腰,后面我們統(tǒng)一分析上報監(jiān)控平臺狰住。
- 為什么用代理連接?
最主要的就是return 的這一句代碼了吧齿梁。我們說過poolEntry
是底層數(shù)據(jù)庫連接的一個包裝類催植,代表一個數(shù)據(jù)庫連接。那么從createProxyConnection
字面來看勺择,這個方法并不是直接返回數(shù)據(jù)庫連接給用戶使用创南,而是創(chuàng)建了一個代理連接,這個代理連接是什么酵幕?為什么不直接返回數(shù)據(jù)庫連接給用戶使用扰藕?
不管我們使用 Spring 還是自己寫的代碼從 HikariCP 連接池里拿連接,都是拿到一個java.sql.Connection
類型的對象沒錯吧芳撒?它是一個 java 統(tǒng)一的數(shù)據(jù)庫連接接口邓深,不管你使用的是 mysql 還是oracle 等數(shù)據(jù)庫,都是統(tǒng)一對接這個接口笔刹,都必須返回一個這個類型的連接給用戶使用芥备,相當(dāng)于一個門面模式的設(shè)計,這樣用戶可以不理會底層使用什么數(shù)據(jù)庫舌菜,代碼都是一個樣的萌壳。既然如此,HikariCP應(yīng)該直接返回一個java.sql.Connection
對吧?
沒有那么簡單袱瓮。試想一下缤骨,假如 HikariCP 直接返回底層的數(shù)據(jù)庫連接給用戶使用,那么尺借,如果用戶自己關(guān)閉了這個底層數(shù)據(jù)庫連接呢绊起?那么這個連接在連接池里相當(dāng)于已經(jīng)不可用了,其他線程也使用不了了燎斩。作為一個框架設(shè)計者虱歪,不能指望每個用戶都是高手,他們都能在用完數(shù)據(jù)庫連接不會關(guān)閉它并且要還回連接池中栅表,肯定有小白用戶或者很唬的不管三七二十一的人笋鄙。更何況除了關(guān)閉連接,還有你修改了連接的設(shè)置呢怪瓶,比如自動提交事務(wù)萧落,連接只讀這些設(shè)置,然后沒有恢復(fù)回原來的設(shè)置怎么辦劳殖?如此混亂的話铐尚,我們使用連接池就沒有意義了。所以我們不能把底層數(shù)據(jù)庫連接直接給用戶使用哆姻,這個大家理解了吧宣增?
如何來實(shí)現(xiàn)呢?我們可以繼承java.sql.Connection
矛缨,創(chuàng)建一個它的子類爹脾,子類可以直接當(dāng)做父類來用,沒錯吧箕昭?然后我們在子類里覆蓋java.sql.Connection
里面敏感的操作灵妨,比如關(guān)閉連接,如果用戶調(diào)用了關(guān)閉連接操作落竹,不是真正的關(guān)閉底層連接泌霍,而是將連接還回到連接池。怎么樣述召?我們解決了用戶瞎用的問題了吧朱转。作者就是這個目的,才設(shè)計了一個createProxyConnection
方法來創(chuàng)建了一個連接的代理ProxyConnection
积暖,將這個代理返回給用戶使用藤为。一切如我們所說的,ProxyConnection
繼承了java.sql.Connection
夺刑,覆蓋了一些方法缅疟,詳細(xì)的我們后面單獨(dú)的文章解析分别,這里很重要。
- 泄露檢測
我之前寫過一個連接泄露檢測的文章存淫,是我寫的瀏覽量最大的文章耘斩,這說明,有不少人都遇到這個問題纫雁。在 HikariCP 檢測到連接泄露的時候煌往,會拋出一個 warn:java.lang.Exception: Apparent connection leak detected
。我們在這里詳細(xì)說一下這個地方的邏輯轧邪。
- 連接泄露檢測的相關(guān)配置
有一個leakDetectionThreshold
的配置,這個就是連接泄露檢測的最大時間羞海,默認(rèn)是 0忌愚,表示不啟用泄露檢測;最小值 2000 毫秒却邓,如果用戶設(shè)置的小于 2000 毫秒硕糊,默認(rèn)關(guān)閉泄露檢測,最大值不能超過連接的最大存活時間腊徙,也就是maxLifetime配置简十,超過的話也會自動禁用泄露檢測。
- 泄露檢測的定時任務(wù)
在createProxyConnection
方法中撬腾,我們可以看到傳了一個參數(shù)leakTask.schedule(poolEntry)
螟蝙。leakTask
的類型是ProxyLeakTask
,它實(shí)現(xiàn)了Runnable
接口民傻,是一個多線程的定時任務(wù)實(shí)現(xiàn)胰默。它的內(nèi)部持有幾個成員變量:ScheduledExecutorService
,是用來執(zhí)行泄露檢測定時任務(wù)的線程池漓踢;leakDetectionThreshold
牵署,是泄露檢測超時時間;
scheduledFuture
是任務(wù)的 future 結(jié)果喧半,可以用來取消定時任務(wù)奴迅。
我們看下它的schedule
方法:
ProxyLeakTask schedule(final PoolEntry bagEntry) {
return (leakDetectionThreshold == 0) ? NO_LEAK : new ProxyLeakTask(this, bagEntry);
}
這里判斷了下用戶有沒有開啟泄露檢測功能,如果是沒有開啟挺据,那么就返回一個NO_LEAK
取具。大家還記得FAUX_LOCK
吧?就是上面的①處令牌桶的實(shí)現(xiàn)吴菠,是提供了一個空實(shí)現(xiàn)對吧者填?這里也是同樣的道理,NO_LEAK
是一個空實(shí)現(xiàn)做葵,如果用戶沒有開啟泄露檢測就方便 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òu)造方法,第一個參數(shù)也是一個ProxyLeakTask
榨乎,看名字parent
是個父任務(wù)怎燥。這個父任務(wù)在連接池初始化的時候會創(chuàng)建,創(chuàng)建的時候需要兩個參數(shù)蜜暑,一個是用于執(zhí)行任務(wù)的線程池executorService
铐姚,另一個是連接泄露超時時間leakDetectionThreshold
。此處傳遞父任務(wù)進(jìn)來就是要使用父任務(wù)中的線程池和連接泄露超時時間肛捍。
我們看下超時檢測的任務(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);
}
由于這里不太重要隐绵,我們就不一句一句的分析了,整個run
方法就是構(gòu)造一個異常拙毫,然后拋出一個 warn 異常棧依许。
到此,我們整個連接泄露的分析就結(jié)束了缀蹄。
- 釋放鎖
有一個需要注意的是峭跳,我們在最開始的第一句,是申請了一個令牌缺前,現(xiàn)在上面已經(jīng)獲取到了可用連接蛀醉,我們需要釋放這個令牌。我們在使用其他鎖的時候也是一樣的衅码,一定要在最后釋放鎖拯刁,為了防止任何異常打斷代碼執(zhí)行,所以釋放鎖的代碼一定要放在 finally 中肆良,保證最后一定會把鎖釋放掉筛璧。
⑤獲取連接超時
上面整個獲取連接的過程②③④代碼是放在 do-while 中來執(zhí)行的,只要不超過設(shè)置的connectionTimeout
惹恃,就會一直嘗試循環(huán)獲取連接夭谤,直到超過了connectionTimeout
,就會執(zhí)行⑤的代碼巫糙。超時之后有兩個步驟:一是向監(jiān)控平臺上報獲取連接超時朗儒;二是構(gòu)造一個異常信息,然后拋出去参淹。
至此醉锄,整個獲取連接的邏輯就介紹完了,可能有一些沒有說到的細(xì)節(jié)浙值,大家可以發(fā)表意見恳不,我們一起學(xué)習(xí)討論。