如今已經有不少公司在使用HikariCP了,HikariCP還成爲了SpringBoot默認的鏈接池,伴隨着SpringBoot和微服務,HikariCP 必將迎來普遍的普及。java
下面陳某帶你們從源碼角度分析一下HikariCP爲何可以被Spring Boot 青睞,文章目錄以下:mysql
目錄sql
開始前先來了解下HikariCP獲取一個鏈接時類間的交互流程,方便下面詳細流程的閱讀。緩存
獲取鏈接時的類間交互:網絡
圖1多線程
HikariCP獲取鏈接時的入口是HikariDataSource裏的getConnection方法,如今來看下該方法的具體流程:併發
主流程1框架
上述爲HikariCP獲取鏈接時的流程圖,由圖1可知,每一個datasource對象裏都會持有一個HikariPool對象,記爲pool,初始化後的datasource對象pool是空的,因此第一次getConnection的時候會進行實例化pool屬性(參考主流程1),初始化的時候須要將當前datasource裏的config屬性傳過去,用於pool的初始化,最終標記sealed,而後根據pool對象調用getConnection方法(參考流程1.1),獲取成功後返回鏈接對象。dom
主流程2異步
該流程用於初始化整個鏈接池,這個流程會給鏈接池內全部的屬性作初始化的工做,其中比較主要的幾個流程上圖已經指出,簡單歸納一下:
利用config初始化各類鏈接池屬性,而且產生一個用於生產物理鏈接的數據源DriverDataSource
初始化存放鏈接對象的核心類connectionBag
初始化一個延時任務線程池類型的對象houseKeepingExecutorService,用於後續執行一些延時/定時類任務(好比鏈接泄漏檢查延時任務,參考流程2.2以及主流程4,除此以外maxLifeTime後主動回收關閉鏈接也是交由該對象來執行的,這個過程能夠參考主流程3)
預熱鏈接池,HikariCP會在該流程的checkFailFast裏初始化好一個鏈接對象放進池子內,固然觸發該流程得保證initializationTimeout > 0時(默認值1),這個配置屬性表示留給預熱操做的時間(默認值1在預熱失敗時不會發生重試)。與Druid經過initialSize控制預熱鏈接對象數不同的是,HikariCP僅預熱進池一個鏈接對象。
初始化一個線程池對象addConnectionExecutor,用於後續擴充鏈接對象
初始化一個線程池對象closeConnectionExecutor,用於關閉一些鏈接對象,怎麼觸發關閉任務呢?能夠參考流程1.1.2
流程1.1
從最開始的結構圖可知,每一個HikariPool裏都維護一個ConcurrentBag對象,用於存放鏈接對象,由上圖能夠看到,實際上HikariPool的getConnection就是從ConcurrentBag裏獲取鏈接的(調用其borrow方法得到,對應ConnectionBag主流程),在長鏈接檢查這塊,與以前說的Druid不一樣,這裏的長鏈接判活檢查在鏈接對象沒有被標記爲「已丟棄」時,只要距離上次使用超過500ms每次取出都會進行檢查(500ms是默認值,可經過配置com.zaxxer.hikari.aliveBypassWindowMs的系統參數來控制),emmmm,也就是說HikariCP對長鏈接的活性檢查很頻繁,可是其併發性能依舊優於Druid,說明頻繁的長鏈接檢查並非致使鏈接池性能高低的關鍵所在。
這個實際上是因爲HikariCP的無鎖實現,在高併發時對CPU的負載沒有其餘鏈接池那麼高而產生的併發性能差別,後面會說HikariCP的具體作法,即便是Druid,在獲取鏈接、生成鏈接、歸還鏈接時都進行了鎖控制,由於經過上篇解析Druid的文章能夠知道,Druid裏的鏈接池資源是多線程共享的,不可避免的會有鎖競爭,有鎖競爭意味着線程狀態的變化會很頻繁,線程狀態變化頻繁意味着CPU上下文切換也將會很頻繁。
回到流程1.1,若是拿到的鏈接爲空,直接報錯,不爲空則進行相應的檢查,若是檢查經過,則包裝成ConnectionProxy對象返回給業務方,不經過則調用closeConnection方法關閉鏈接(對應流程1.1.2,該流程會觸發ConcurrentBag的remove方法丟棄該鏈接,而後把實際的驅動鏈接交給closeConnectionExecutor線程池,異步關閉驅動鏈接)。
流程1.1.1
承接上面的流程1.1裏的判活流程,來看下判活是如何作的,首先說驗證方法(注意這裏該方法接受的這個connection對象不是poolEntry,而是poolEntry持有的實際驅動的鏈接對象),在以前介紹Druid的時候就知道,Druid是根據驅動程序裏是否存在ping方法來判斷是否啓用ping的方式判斷鏈接是否存活,可是到了HikariCP則更加簡單粗暴,僅根據是否配置了connectionTestQuery覺定是否啓用ping:
this.isUseJdbc4Validation = config.getConnectionTestQuery() == null;
因此通常驅動若是不是特別低的版本,不建議配置該項,不然便會走createStatement+excute的方式,相比ping簡單發送心跳數據,這種方式顯然更低效。
此外,這裏在剛進來還會經過驅動的鏈接對象從新給它設置一遍networkTimeout的值,使之變成validationTimeout,表示一次驗證的超時時間,爲啥這裏要從新設置這個屬性呢?由於在使用ping方法校驗時,是沒辦法經過相似statement那樣能夠setQueryTimeout的,因此只能由網絡通訊的超時時間來控制,這個時間能夠經過jdbc的鏈接參數socketTimeout來控制:
jdbc:mysql://127.0.0.1:3306/xxx?socketTimeout=250
這個值最終會被賦值給HikariCP的networkTimeout字段,這就是爲何最後那一步使用這個字段來還原驅動鏈接超時屬性的緣由;說到這裏,最後那裏爲啥要再次還原呢?這就很容易理解了,由於驗證結束了,鏈接對象還存活的狀況下,它的networkTimeout的值這時仍然等於validationTimeout(不合預期),顯然在拿出去用以前,須要恢復成原本的值,也就是HikariCP裏的networkTimeout屬性。
流程1.1.2
這個流程簡單來講就是把流程1.1.1中驗證不經過的死鏈接,主動關閉的一個流程,首先會把這個鏈接對象從ConnectionBag裏移除,而後把實際的物理鏈接交給一個線程池去異步執行,這個線程池就是在主流程2裏初始化池的時候初始化的線程池closeConnectionExecutor,而後異步任務內開始實際的關鏈接操做,由於主動關閉了一個鏈接至關於少了一個鏈接,因此還會觸發一次擴充鏈接池(參考主流程5)操做。
不一樣於Druid那樣監控指標那麼多,HikariCP會把咱們很是關心的幾項指標暴露給咱們,好比當前鏈接池內閒置鏈接數、總鏈接數、一個鏈接被用了多久歸還、建立一個物理鏈接花費多久等,HikariCP的鏈接池的監控咱們這一節專門詳細的分解一下,首先找到HikariCP下面的metrics文件夾,這下面放置了一些規範實現的監控接口等,還有一些現成的實現(好比HikariCP自帶對prometheus、micrometer、dropwizard的支持,不太瞭解後面兩個,prometheus下文直接稱爲普羅米修斯):
圖2
下面,來着重看下接口的定義:
//這個接口的實現主要負責收集一些動做的耗時
public interface IMetricsTracker extends AutoCloseable
{
//這個方法觸發點在建立實際的物理鏈接時(主流程3),用於記錄一個實際的物理鏈接建立所耗費的時間
default void recordConnectionCreatedMillis(long connectionCreatedMillis) {}
//這個方法觸發點在getConnection時(主流程1),用於記錄獲取一個鏈接時實際的耗時
default void recordConnectionAcquiredNanos(final long elapsedAcquiredNanos) {}
//這個方法觸發點在回收鏈接時(主流程6),用於記錄一個鏈接從被獲取到被回收時所消耗的時間
default void recordConnectionUsageMillis(final long elapsedBorrowedMillis) {}
//這個方法觸發點也在getConnection時(主流程1),用於記錄獲取鏈接超時的次數,每發生一次獲取鏈接超時,就會觸發一次該方法的調用
default void recordConnectionTimeout() {}
@Override
default void close() {}
}
觸發點都瞭解清楚後,再來看看MetricsTrackerFactory的接口定義:
//用於建立IMetricsTracker實例,而且按需記錄PoolStats對象裏的屬性(這個對象裏的屬性就是相似鏈接池當前閒置鏈接數之類的線程池狀態類指標)
public interface MetricsTrackerFactory
{
//返回一個IMetricsTracker對象,而且把PoolStats傳了過去
IMetricsTracker create(String poolName, PoolStats poolStats);
}
上面的接口用法見註釋,針對新出現的PoolStats類,咱們來看看它作了什麼:
public abstract class PoolStats {
private final AtomicLong reloadAt; //觸發下次刷新的時間(時間戳)
private final long timeoutMs; //刷新下面的各項屬性值的頻率,默認1s,沒法改變
// 總鏈接數
protected volatile int totalConnections;
// 閒置鏈接數
protected volatile int idleConnections;
// 活動鏈接數
protected volatile int activeConnections;
// 因爲沒法獲取到可用鏈接而阻塞的業務線程數
protected volatile int pendingThreads;
// 最大鏈接數
protected volatile int maxConnections;
// 最小鏈接數
protected volatile int minConnections;
public PoolStats(final long timeoutMs) {
this.timeoutMs = timeoutMs;
this.reloadAt = new AtomicLong();
}
//這裏以獲取最大鏈接數爲例,其餘的跟這個差很少
public int getMaxConnections() {
if (shouldLoad()) { //是否應該刷新
update(); //刷新屬性值,注意這個update的實如今HikariPool裏,由於這些屬性值的直接或間接來源都是HikariPool
}
return maxConnections;
}
protected abstract void update(); //實如今↑上面已經說了
private boolean shouldLoad() { //按照更新頻率來決定是否刷新屬性值
for (; ; ) {
final long now = currentTime();
final long reloadTime = reloadAt.get();
if (reloadTime > now) {
return false;
} else if (reloadAt.compareAndSet(reloadTime, plusMillis(now, timeoutMs))) {
return true;
}
}
}
}
實際上這裏就是這些屬性獲取和觸發刷新的地方,那麼這個對象是在哪裏被生成而且丟給MetricsTrackerFactory的create方法的呢?這就是本節所須要講述的要點:主流程2裏的設置監控器的流程,來看看那裏發生了什麼事吧:
//監控器設置方法(此方法在HikariPool中,metricsTracker屬性就是HikariPool用來觸發IMetricsTracker裏方法調用的)
public void setMetricsTrackerFactory(MetricsTrackerFactory metricsTrackerFactory) {
if (metricsTrackerFactory != null) {
//MetricsTrackerDelegate是包裝類,是HikariPool的一個靜態內部類,是實際持有IMetricsTracker對象的類,也是實際觸發IMetricsTracker裏方法調用的類
//這裏首先會觸發MetricsTrackerFactory類的create方法拿到IMetricsTracker對象,而後利用getPoolStats初始化PoolStat對象,而後也一併傳給MetricsTrackerFactory
this.metricsTracker = new MetricsTrackerDelegate(metricsTrackerFactory.create(config.getPoolName(), getPoolStats()));
} else {
//不啓用監控,直接等於一個沒有實現方法的空類
this.metricsTracker = new NopMetricsTrackerDelegate();
}
}
private PoolStats getPoolStats() {
//初始化PoolStats對象,而且規定1s觸發一次屬性值刷新的update方法
return new PoolStats(SECONDS.toMillis(1)) {
@Override
protected void update() {
//實現了PoolStat的update方法,刷新各個屬性的值
this.pendingThreads = HikariPool.this.getThreadsAwaitingConnection();
this.idleConnections = HikariPool.this.getIdleConnections();
this.totalConnections = HikariPool.this.getTotalConnections();
this.activeConnections = HikariPool.this.getActiveConnections();
this.maxConnections = config.getMaximumPoolSize();
this.minConnections = config.getMinimumIdle();
}
};
}
到這裏HikariCP的監控器就算是註冊進去了,因此要想實現本身的監控器拿到上面的指標,要通過以下步驟:
新建一個類實現IMetricsTracker接口,咱們這裏將該類記爲IMetricsTrackerImpl
新建一個類實現MetricsTrackerFactory接口,咱們這裏將該類記爲MetricsTrackerFactoryImpl,而且將上面的IMetricsTrackerImpl在其create方法內實例化
將MetricsTrackerFactoryImpl實例化後調用HikariPool的setMetricsTrackerFactory方法註冊到Hikari鏈接池。
上面沒有提到PoolStats裏的屬性怎麼監控,這裏來講下,因爲create方法是調用一次就沒了,create方法只是接收了PoolStats對象的實例,若是不處理,那麼隨着create調用的結束,這個實例針對監控模塊來講就失去持有了,因此這裏若是想要拿到PoolStats裏的屬性,就須要開啓一個守護線程,讓其持有PoolStats對象實例,而且定時獲取其內部屬性值,而後push給監控系統,若是是普羅米修斯等使用pull方式獲取監控數據的監控系統,能夠效仿HikariCP原生普羅米修斯監控的實現,自定義一個Collector對象來接收PoolStats實例,這樣普羅米修斯就能夠按期拉取了,好比HikariCP根據普羅米修斯監控系統本身定義的MetricsTrackerFactory實現(對應圖2裏的PrometheusMetricsTrackerFactory類):
@Override
public IMetricsTracker create(String poolName, PoolStats poolStats) {
getCollector().add(poolName, poolStats); //將接收到的PoolStats對象直接交給Collector,這樣普羅米修斯服務端每觸發一次採集接口的調用,PoolStats都會跟着執行一遍內部屬性獲取流程
return new PrometheusMetricsTracker(poolName, this.collectorRegistry); //返回IMetricsTracker接口的實現類
}
//自定義的Collector
private HikariCPCollector getCollector() {
if (collector == null) {
//註冊到普羅米修斯收集中心
collector = new HikariCPCollector().register(this.collectorRegistry);
}
return collector;
經過上面的解釋能夠知道在HikariCP中如何自定義一個本身的監控器,以及相比Druid的監控,有什麼區別。工做中不少時候都是須要自定義的,我司雖然也是用的普羅米修斯監控,可是由於HikariCP原生的普羅米修斯收集器裏面對監控指標的命名並不符合我司的規範,因此就自定義了一個,有相似問題的不妨也試一試。
???? 這一節沒有畫圖,純代碼,由於畫圖不太好解釋這部分的東西,這部份內容與鏈接池總體流程關係也不大,充其量獲取了鏈接池自己的一些屬性,在鏈接池裏的觸發點也在上面代碼段的註釋裏說清楚了,看代碼定義可能更好理解一些。
本節對應主流程2裏的子流程2.2,在初始化池對象時,初始化了一個叫作leakTaskFactory的屬性,本節來看下它具體是用來作什麼的。
一個鏈接被拿出去使用時間超過leakDetectionThreshold(可配置,默認0)未歸還的,會觸發一個鏈接泄漏警告,通知業務方目前存在鏈接泄漏的問題。
該屬性是ProxyLeakTaskFactory類型對象,且它還會持有houseKeepingExecutorService這個線程池對象,用於生產ProxyLeakTask對象,而後利用上面的houseKeepingExecutorService延時運行該對象裏的run方法。該流程的觸發點在上面的流程1.1最後包裝成ProxyConnection對象的那一步,來看看具體的流程圖:
流程2.2
每次在流程1.1那裏生成ProxyConnection對象時,都會觸發上面的流程,由流程圖能夠知道,ProxyConnection對象持有PoolEntry和ProxyLeakTask的對象,其中初始化ProxyLeakTask對象時就用到了leakTaskFactory對象,經過其schedule方法能夠進行ProxyLeakTask的初始化,並將其實例傳遞給ProxyConnection進行初始化賦值(ps:由圖知ProxyConnection在觸發回收事件時,會主動取消這個泄漏檢查任務,這也是ProxyConnection須要持有ProxyLeakTask對象的緣由)。
在上面的流程圖中能夠知道,只有在leakDetectionThreshold不等於0的時候纔會生成一個帶有實際延時任務的ProxyLeakTask對象,不然返回無實際意義的空對象。因此要想啓用鏈接泄漏檢查,首先要把leakDetectionThreshold配置設置上,這個屬性表示通過該時間後借出去的鏈接仍未歸還,則觸發鏈接泄漏告警。
ProxyConnection之因此要持有ProxyLeakTask對象,是由於它能夠監聽到鏈接是否觸發歸還操做,若是觸發,則調用cancel方法取消延時任務,防止誤告。
由此流程能夠知道,跟Druid同樣,HikariCP也有鏈接對象泄漏檢查,與Druid主動回收鏈接相比,HikariCP實現更加簡單,僅僅是在觸發時打印警告日誌,不會採起具體的強制回收的措施。
與Druid同樣,默認也是關閉這個流程的,由於實際開發中通常使用第三方框架,框架自己會保證及時的close鏈接,防止鏈接對象泄漏,開啓與否仍是取決於業務是否須要,若是必定要開啓,如何設置leakDetectionThreshold的大小也是須要考慮的一件事。
本節來說下主流程2裏的createEntry方法,這個方法利用PoolBase裏的DriverDataSource對象生成一個實際的鏈接對象(若是忘記DriverDatasource是哪裏初始化的了,能夠看下主流程2裏PoolBase的initializeDataSource方法的做用),而後用PoolEntry類包裝成PoolEntry對象,如今來看下這個包裝類有哪些主要屬性:
final class PoolEntry implements IConcurrentBagEntry {
private static final Logger LOGGER = LoggerFactory.getLogger(PoolEntry.class);
//經過cas來修改state屬性
private static final AtomicIntegerFieldUpdater stateUpdater;
Connection connection; //實際的物理鏈接對象
long lastAccessed; //觸發回收時刷新該時間,表示「最近一次使用時間」
long lastBorrowed; //getConnection裏borrow成功後刷新該時間,表示「最近一次借出的時間」
@SuppressWarnings("FieldCanBeLocal")
private volatile int state = 0; //鏈接狀態,枚舉值:IN_USE(使用中)、NOT_IN_USE(閒置中)、REMOVED(已移除)、RESERVED(標記爲保留中)
private volatile boolean evict; //是否被標記爲廢棄,不少地方用到(好比流程1.1靠這個判斷鏈接是否已被廢棄,再好比主流程4裏時鐘回撥時觸發的直接廢棄邏輯)
private volatile ScheduledFuture<?> endOfLife; //用於在超過鏈接生命週期(maxLifeTime)時廢棄鏈接的延時任務,這裏poolEntry要持有該對象,主要是由於在對象主動被關閉時(意味着不須要在超過maxLifeTime時主動失效了),須要cancel掉該任務
private final FastList openStatements; //當前該鏈接對象上生成的全部的statement對象,用於在回收鏈接時主動關閉這些對象,防止存在漏關的statement
private final HikariPool hikariPool; //持有pool對象
private final boolean isReadOnly; //是否爲只讀
private final boolean isAutoCommit; //是否存在事務
}
上面就是整個PoolEntry對象裏全部的屬性,這裏再說下endOfLife對象,它是一個利用houseKeepingExecutorService這個線程池對象作的延時任務,這個延時任務通常在建立好鏈接對象後maxLifeTime左右的時間觸發,具體來看下createEntry代碼:
private PoolEntry createPoolEntry() {
final PoolEntry poolEntry = newPoolEntry(); //生成實際的鏈接對象
final long maxLifetime = config.getMaxLifetime(); //拿到配置好的maxLifetime
if (maxLifetime > 0) { //<=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.setFutureEol(houseKeepingExecutorService.schedule(
() -> { //實際的延時任務,這裏直接觸發softEvictConnection,而softEvictConnection內則會標記該鏈接對象爲廢棄狀態,而後嘗試修改其狀態爲STATE_RESERVED,若成功,則觸發closeConnection(對應流程1.1.2)
if (softEvictConnection(poolEntry, "(connection has passed maxLifetime)", false /* not owner */)) {
addBagItem(connectionBag.getWaitingThreadCount()); //回收完畢後,鏈接池內少了一個鏈接,就會嘗試新增一個鏈接對象
}
},
lifetime, MILLISECONDS)); //給endOfLife賦值,而且提交延時任務,lifetime後觸發
}
return poolEntry;
}
//觸發新增鏈接任務
public void addBagItem(final int waiting) {
//前排提示:addConnectionQueue和addConnectionExecutor的關係和初始化參考主流程2
//當添加鏈接的隊列裏已提交的任務超過那些由於獲取不到鏈接而發生阻塞的線程個數時,就進行提交鏈接新增鏈接的任務
final boolean shouldAdd = waiting - addConnectionQueue.size() >= 0; // Yes, >= is intentional.
if (shouldAdd) {
//提交任務給addConnectionExecutor這個線程池,PoolEntryCreator是一個實現了Callable接口的類,下面將經過流程圖的方式介紹該類的call方法
addConnectionExecutor.submit(poolEntryCreator);
}
}
經過上面的流程,能夠知道,HikariCP通常經過createEntry方法來新增一個鏈接入池,每一個鏈接被包裝成PoolEntry對象,在建立好對象時,同時會提交一個延時任務來關閉廢棄該鏈接,這個時間就是咱們配置的maxLifeTime,爲了保證不在同一時間失效,HikariCP還會利用maxLifeTime減去一個隨機數做爲最終的延時任務延遲時間,而後在觸發廢棄任務時,還會觸發addBagItem,進行鏈接添加任務(由於廢棄了一個鏈接,須要往池子裏補充一個),該任務則交給由主流程2裏定義好的addConnectionExecutor線程池執行,那麼,如今來看下這個異步添加鏈接對象的任務流程:
addConnectionExecutor的call流程
這個流程就是往鏈接池裏加鏈接用的,跟createEntry結合起來講是由於這倆流程是緊密相關的,除此以外,主流程5(fillPool,擴充鏈接池)也會觸發該任務。
HikariCP會按照minIdle定時清理閒置太久的鏈接,這個定時任務在主流程2初始化鏈接池對象時被啓用,跟上面的流程同樣,也是利用houseKeepingExecutorService這個線程池對象作該定時任務的執行器。
來看下主流程2裏是怎麼啓用該任務的:
//housekeepingPeriodMs的默認值是30s,因此定時任務的間隔爲30s
this.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, housekeepingPeriodMs, MILLISECONDS);
那麼本節主要來講下HouseKeeper這個類,該類實現了Runnable接口,回收邏輯主要在其run方法內,來看看run方法的邏輯流程圖:
主流程4:鏈接池縮容
上面的流程就是HouseKeeper的run方法裏具體作的事情,因爲系統時間回撥會致使該定時任務回收一些鏈接時產生偏差,所以存在以下判斷:
//now就是當前系統時間,previous就是上次觸發該任務時的時間,housekeepingPeriodMs就是隔多久觸發該任務一次
//也就是說plusMillis(previous, housekeepingPeriodMs)表示當前時間
//若是系統時間沒被回撥,那麼plusMillis(now, 128)必定是大於當前時間的,若是被系統時間被回撥
//回撥的時間超過128ms,那麼下面的判斷就成立,不然永遠不會成立
if (plusMillis(now, 128) < plusMillis(previous, housekeepingPeriodMs))
這是hikariCP在解決系統時鐘被回撥時作出的一種措施,經過流程圖能夠看到,它是直接把池子裏全部的鏈接對象取出來挨個兒的標記成廢棄,而且嘗試把狀態值修改成STATE_RESERVED(後面會說明這些狀態,這裏先不深究)。若是系統時鐘沒有發生改變(絕大多數狀況會命中這一塊的邏輯),由圖知,會把當前池內全部處於閒置狀態(STATE_NOT_IN_USE)的鏈接拿出來,而後計算須要檢查的範圍,而後循環着修改鏈接的狀態:
//拿到全部處於閒置狀態的鏈接
final List notInUse = connectionBag.values(STATE_NOT_IN_USE);
//計算出須要被檢查閒置時間的數量,簡單來講,池內須要保證最小minIdle個鏈接活着,因此須要計算出超出這個範圍的閒置對象進行檢查
int toRemove = notInUse.size() - config.getMinIdle();
for (PoolEntry entry : notInUse) {
//在檢查範圍內,且閒置時間超出idleTimeout,而後嘗試將鏈接對象狀態由STATE_NOT_IN_USE變爲STATE_RESERVED成功
if (toRemove > 0 && elapsedMillis(entry.lastAccessed, now) > idleTimeout && connectionBag.reserve(entry)) {
closeConnection(entry, "(connection has passed idleTimeout)"); //知足上述條件,進行鏈接關閉
toRemove--;
}
}
fillPool(); //由於可能回收了一些鏈接,因此要再次觸發鏈接池擴充流程檢查下是否須要新增鏈接。
上面的代碼就是流程圖裏對應的沒有回撥系統時間時的流程邏輯。該流程在idleTimeout大於0(默認等於0)而且minIdle小於maxPoolSize的時候纔會啓用,默認是不啓用的,若須要啓用,能夠按照條件來配置。
這個流程主要依附HikariPool裏的fillPool方法,這個方法已經在上面不少流程裏出現過了,它的做用就是在觸發鏈接廢棄、鏈接池鏈接不夠用時,發起擴充鏈接數的操做,這是個很簡單的過程,下面看下源碼(爲了使代碼結構更加清晰,對源碼作了細微改動):
// PoolEntryCreator關於call方法的實現流程在主流程3裏已經看過了,可是這裏卻有倆PoolEntryCreator對象,
// 這是個較細節的地方,用於打日誌用,再也不說這部分,爲了便於理解,只須要知道這倆對象執行的是同一塊call方法便可
private final PoolEntryCreator poolEntryCreator = new PoolEntryCreator(null);
private final PoolEntryCreator postFillPoolEntryCreator = new PoolEntryCreator("After adding ");
private synchronized void fillPool() {
// 這個判斷就是根據當前池子裏相關數據,推算出須要擴充的鏈接數,
// 判斷方式就是利用最大鏈接數跟當前鏈接總數的差值,與最小鏈接數與當前池內閒置的鏈接數的差值,取其最小的那一個獲得
int needAdd = Math.min(maxPoolSize - connectionBag.size(),
minIdle - connectionBag.getCount(STATE_NOT_IN_USE));
//減去當前排隊的任務,就是最終須要新增的鏈接數
final int connectionsToAdd = needAdd - addConnectionQueue.size();
for (int i = 0; i < connectionsToAdd; i++) {
//通常循環的最後一次會命中postFillPoolEntryCreator任務,其實就是在最後一次會打印一第二天志而已(能夠忽略該干擾邏輯)
addConnectionExecutor.submit((i < connectionsToAdd - 1) ? poolEntryCreator : postFillPoolEntryCreator);
}
}
由該過程能夠知道,最終這個新增鏈接的任務也是交由addConnectionExecutor線程池來處理的,而任務的主題也是PoolEntryCreator,這個流程能夠參考主流程3.
而後needAdd的推算:
Math.min(最大鏈接數 - 池內當前鏈接總數, 最小鏈接數 - 池內閒置的鏈接數)
根據這個方式判斷,能夠保證池內的鏈接數永遠不會超過maxPoolSize,也永遠不會低於minIdle。在鏈接吃緊的時候,能夠保證每次觸發都以minIdle的數量擴容。所以若是在maxPoolSize跟minIdle配置的值同樣的話,在池內鏈接吃緊的時候,就不會發生任何擴容了。
最開始說過,最終真實的物理鏈接對象會被包裝成PoolEntry對象,存放進ConcurrentBag,而後獲取時,PoolEntry對象又會被再次包裝成ProxyConnection對象暴露給使用方的,那麼觸發鏈接回收,實際上就是觸發ProxyConnection裏的close方法:
public final void close() throws SQLException {
// 原註釋:Closing statements can cause connection eviction, so this must run before the conditional below
closeStatements(); //此鏈接對象在業務方使用過程當中產生的全部statement對象,進行統一close,防止漏close的狀況
if (delegate != ClosedConnection.CLOSED_CONNECTION) {
leakTask.cancel(); //取消鏈接泄漏檢查任務,參考流程2.2
try {
if (isCommitStateDirty && !isAutoCommit) { //在存在執行語句後而且還打開了事務,調用close時須要主動回滾事務
delegate.rollback(); //回滾
lastAccess = currentTime(); //刷新"最後一次使用時間"
}
} finally {
delegate = ClosedConnection.CLOSED_CONNECTION;
poolEntry.recycle(lastAccess); //觸發回收
}
}
}
這個就是ProxyConnection裏的close方法,能夠看到它最終會調用PoolEntry的recycle方法進行回收,除此以外,鏈接對象的最後一次使用時間也是在這個時候刷新的,該時間是個很重要的屬性,能夠用來判斷一個鏈接對象的閒置時間,來看下PoolEntry的recycle方法:
void recycle(final long lastAccessed) {
if (connection != null) {
this.lastAccessed = lastAccessed; //刷新最後使用時間
hikariPool.recycle(this); //觸發HikariPool的回收方法,把本身傳過去
}
}
以前有說過,每一個PoolEntry對象都持有HikariPool的對象,方便觸發鏈接池的一些操做,由上述代碼能夠看到,最終仍是會觸發HikariPool裏的recycle方法,再來看下HikariPool的recycle方法:
void recycle(final PoolEntry poolEntry) {
metricsTracker.recordConnectionUsage(poolEntry); //監控指標相關,忽略
connectionBag.requite(poolEntry); //最終觸發connectionBag的requite方法歸還鏈接,該流程參考ConnectionBag主流程裏的requite方法部分
}
以上就是鏈接回收部分的邏輯,相比其餘流程,仍是比較簡潔的。
這個類用來存放最終的PoolEntry類型的鏈接對象,提供了基本的增刪查的功能,被HikariPool持有,上面那麼多的操做,幾乎都是在HikariPool中完成的,HikariPool用來管理實際的鏈接生產動做和回收動做,實際操做的倒是ConcurrentBag類,梳理下上面全部流程的觸發點:
主流程2:初始化HikariPool時初始化ConcurrentBag(構造方法),預熱時經過createEntry拿到鏈接對象,調用ConcurrentBag.add添加鏈接到ConcurrentBag。
流程1.1:經過HikariPool獲取鏈接時,經過調用ConcurrentBag.borrow拿到一個鏈接對象。
主流程6:經過ConcurrentBag.requite歸還一個鏈接。
流程1.1.2:觸發關閉鏈接時,會經過ConcurrentBag.remove移除鏈接對象,由前面的流程可知關閉鏈接觸發點爲:鏈接超過最大生命週期maxLifeTime主動廢棄、健康檢查不經過主動廢棄、鏈接池縮容。
主流程3:經過異步添加鏈接時,經過調用ConcurrentBag.add添加鏈接到ConcurrentBag,由前面的流程可知添加鏈接觸發點爲:鏈接超過最大生命週期maxLifeTime主動廢棄鏈接後、鏈接池擴容。
主流程4:鏈接池縮容任務,經過調用ConcurrentBag.values篩選出須要的作操做的鏈接對象,而後再經過ConcurrentBag.reserve完成對鏈接對象狀態的修改,而後會經過流程1.1.2觸發關閉和移除鏈接操做。
經過觸發點整理,能夠知道該結構裏的主要方法,就是上面觸發點裏標記爲標籤色的部分,而後來具體看下該類的基本定義和主要方法:
public class ConcurrentBag<T extends IConcurrentBagEntry> implements AutoCloseable {
private final CopyOnWriteArrayList<T> sharedList; //最終存放PoolEntry對象的地方,它是一個CopyOnWriteArrayList
private final boolean weakThreadLocals; //默認false,爲true時可讓一個鏈接對象在下方threadList裏的list內處於弱引用狀態,防止內存泄漏(參見備註1)
private final ThreadLocal<List<Object>> threadList; //線程級的緩存,從sharedList拿到的鏈接對象,會被緩存進當前線程內,borrow時會先從緩存中拿,從而達到池內無鎖實現
private final IBagStateListener listener; //內部接口,HikariPool實現了該接口,主要用於ConcurrentBag主動通知HikariPool觸發添加鏈接對象的異步操做(也就是主流程3裏的addConnectionExecutor所觸發的流程)
private final AtomicInteger waiters; //當前由於獲取不到鏈接而發生阻塞的業務線程數,這個在以前的流程裏也出現過,好比主流程3裏addBagItem就會根據該指標進行判斷是否須要新增鏈接
private volatile boolean closed; //標記當前ConcurrentBag是否已被關閉
private final SynchronousQueue<T> handoffQueue; //這是個即產即銷的隊列,用於在鏈接不夠用時,及時獲取到add方法裏新建立的鏈接對象,詳情能夠參考下面borrow和add的代碼
//內部接口,PoolEntry類實現了該接口
public interface IConcurrentBagEntry {
//鏈接對象的狀態,前面的流程不少地方都已經涉及到了,好比主流程4的縮容
int STATE_NOT_IN_USE = 0; //閒置
int STATE_IN_USE = 1; //使用中
int STATE_REMOVED = -1; //已廢棄
int STATE_RESERVED = -2; //標記保留,介於閒置和廢棄之間的中間狀態,主要由縮容那裏觸發修改
boolean compareAndSet(int expectState, int newState); //嘗試利用cas修改鏈接對象的狀態值
void setState(int newState); //設置狀態值
int getState(); //獲取狀態值
}
//參考上面listener屬性的解釋
public interface IBagStateListener {
void addBagItem(int waiting);
}
//獲取鏈接方法
public T borrow(long timeout, final TimeUnit timeUnit) {
// 省略...
}
//回收鏈接方法
public void requite(final T bagEntry) {
//省略...
}
//添加鏈接方法
public void add(final T bagEntry) {
//省略...
}
//移除鏈接方法
public boolean remove(final T bagEntry) {
//省略...
}
//根據鏈接狀態值獲取當前池子內全部符合條件的鏈接集合
public List values(final int state) {
//省略...
}
//獲取當前池子內全部的鏈接
public List values() {
//省略...
}
//利用cas把傳入的鏈接對象的state從 STATE_NOT_IN_USE 變爲 STATE_RESERVED
public boolean reserve(final T bagEntry) {
//省略...
}
//獲取當前池子內符合傳入狀態值的鏈接數量
public int getCount(final int state) {
//省略...
}
}
從這個基本結構就能夠稍微看出HikariCP是如何優化傳統鏈接池實現的了,相比Druid來講,HikariCP更加偏向無鎖實現,儘可能避免鎖競爭的發生。
這個方法用來獲取一個可用的鏈接對象,觸發點爲流程1.1,HikariPool就是利用該方法獲取鏈接的,下面來看下該方法作了什麼:
public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException {
// 源註釋:Try the thread-local list first
final List<Object> list = threadList.get(); //首先從當前線程的緩存裏拿到以前被緩存進來的鏈接對象集合
for (int i = list.size() - 1; i >= 0; i--) {
final Object entry = list.remove(i); //先移除,回收方法那裏會再次add進來
final T bagEntry = weakThreadLocals ? ((WeakReference<T>) entry).get() : (T) entry; //默認不啓用弱引用
// 獲取到對象後,經過cas嘗試把其狀態從STATE_NOT_IN_USE 變爲 STATE_IN_USE,注意,這裏若是其餘線程也在使用這個鏈接對象,
// 而且成功修改屬性,那麼當前線程的cas會失敗,那麼就會繼續循環嘗試獲取下一個鏈接對象
if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
return bagEntry; //cas設置成功後,表示當前線程繞過其餘線程干擾,成功獲取到該鏈接對象,直接返回
}
}
// 源註釋:Otherwise, scan the shared list ... then poll the handoff queue
final int waiting = waiters.incrementAndGet(); //若是緩存內找不到一個可用的鏈接對象,則認爲須要「回源」,waiters+1
try {
for (T bagEntry : sharedList) {
//循環sharedList,嘗試把鏈接狀態值從STATE_NOT_IN_USE 變爲 STATE_IN_USE
if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
// 源註釋:If we may have stolen another waiter's connection, request another bag add.
if (waiting > 1) { //阻塞線程數大於1時,須要觸發HikariPool的addBagItem方法來進行添加鏈接入池,這個方法的實現參考主流程3
listener.addBagItem(waiting - 1);
}
return bagEntry; //cas設置成功,跟上面的邏輯同樣,表示當前線程繞過其餘線程干擾,成功獲取到該鏈接對象,直接返回
}
}
//走到這裏說明不光線程緩存裏的列表競爭不到鏈接對象,連sharedList裏也找不到可用的鏈接,這時則認爲須要通知HikariPool,該觸發添加鏈接操做了
listener.addBagItem(waiting);
timeout = timeUnit.toNanos(timeout); //這時候開始利用timeout控制獲取時間
do {
final long start = currentTime();
//嘗試從handoffQueue隊列裏獲取最新被加進來的鏈接對象(通常新入的鏈接對象除了加進sharedList以外,還會被offer進該隊列)
final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS);
//若是超出指定時間後仍然沒有獲取到可用的鏈接對象,或者獲取到對象後經過cas設置成功,這兩種狀況都不須要重試,直接返回對象
if (bagEntry == null || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
return bagEntry;
}
//走到這裏說明從隊列內獲取到了鏈接對象,可是cas設置失敗,說明又該對象又被其餘線程率先拿去用了,若時間還夠,則再次嘗試獲取
timeout -= elapsedNanos(start); //timeout減去消耗的時間,表示下次循環可用時間
} while (timeout > 10_000); //剩餘時間大於10s時才繼續進行,通常狀況下,這個循環只會走一次,由於timeout不多會配的比10s還大
return null; //超時,仍然返回null
} finally {
waiters.decrementAndGet(); //這一步出去後,HikariPool收到borrow的結果,算是走出阻塞,因此waiters-1
}
}
仔細看下注釋,該過程大體分紅三個主要步驟:
從線程緩存獲取鏈接
獲取不到再從sharedList裏獲取
都獲取不到則觸發添加鏈接邏輯,並嘗試從隊列裏獲取新生成的鏈接對象
這個流程會添加一個鏈接對象進入bag,一般由主流程3裏的addBagItem方法經過addConnectionExecutor異步任務觸發添加操做,該方法主流程以下:
public void add(final T bagEntry) {
sharedList.add(bagEntry); //直接加到sharedList裏去
// 源註釋:spin until a thread takes it or none are waiting
// 參考borrow流程,當存在線程等待獲取可用鏈接,而且當前新入的這個鏈接狀態仍然是閒置狀態,且隊列裏無消費者等待獲取時,發起一次線程調度
while (waiters.get() > 0 && bagEntry.getState() == STATE_NOT_IN_USE && !handoffQueue.offer(bagEntry)) { //注意這裏會offer一個鏈接對象入隊列
yield();
}
}
結合borrow來理解的話,這裏在存在等待線程時會添加一個鏈接對象入隊列,可讓borrow裏發生等待的地方更容易poll到這個鏈接對象。
這個流程會回收一個鏈接,該方法的觸發點在主流程6,具體代碼以下:
public void requite(final T bagEntry) {
bagEntry.setState(STATE_NOT_IN_USE); //回收意味着使用完畢,更改state爲STATE_NOT_IN_USE狀態
for (int i = 0; waiters.get() > 0; i++) { //若是存在等待線程的話,嘗試傳給隊列,讓borrow獲取
if (bagEntry.getState() != STATE_NOT_IN_USE || handoffQueue.offer(bagEntry)) {
return;
}
else if ((i & 0xff) == 0xff) {
parkNanos(MICROSECONDS.toNanos(10));
}
else {
yield();
}
}
final List<Object> threadLocalList = threadList.get();
if (threadLocalList.size() < 50) { //線程內鏈接集合的緩存最多50個,這裏回收鏈接時會再次加進當前線程的緩存裏,方便下次borrow獲取
threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry); //默認不啓用弱引用,若啓用的話,則緩存集合裏的鏈接對象沒有內存泄露的風險
}
}
這個負責從池子裏移除一個鏈接對象,觸發點在流程1.1.2,代碼以下:
public boolean remove(final T bagEntry) {
// 下面兩個cas操做,都是從其餘狀態變爲移除狀態,任意一個成功,都不會走到下面的warn log
if (!bagEntry.compareAndSet(STATE_IN_USE, STATE_REMOVED) && !bagEntry.compareAndSet(STATE_RESERVED, STATE_REMOVED) && !closed) {
LOGGER.warn("Attempt to remove an object from the bag that was not borrowed or reserved: {}", bagEntry);
return false;
}
// 直接從sharedList移除掉
final boolean removed = sharedList.remove(bagEntry);
if (!removed && !closed) {
LOGGER.warn("Attempt to remove an object from the bag that does not exist: {}", bagEntry);
}
return removed;
}
這裏須要注意的是,移除時僅僅移除了sharedList裏的對象,各個線程內緩存的那一份集合裏對應的對象並無被移除,這個時候會不會存在該鏈接再次從緩存裏拿到呢?會的,可是不會返回出去,而是直接remove掉了,仔細看borrow的代碼發現狀態不是閒置狀態的時候,取出來時就會remove掉,而後也拿不出去,天然也不會觸發回收方法。
該方法存在重載方法,用於返回當前池子內鏈接對象的集合,觸發點在主流程4,代碼以下:
public List values(final int state) {
//過濾出來符合狀態值的對象集合逆序後返回出去
final List list = sharedList.stream().filter(e -> e.getState() == state).collect(Collectors.toList());
Collections.reverse(list);
return list;
}
public List values() {
//返回所有鏈接對象(注意下方clone爲淺拷貝)
return (List) sharedList.clone();
}
該方法單純將鏈接對象的狀態值由STATE_NOT_IN_USE修改成STATE_RESERVED,觸發點仍然是主流程4,縮容時使用,代碼以下:
public boolean reserve(final T bagEntry){
return bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_RESERVED);
}
該方法用於返回池內符合某個狀態值的鏈接的總數量,觸發點爲主流程5,擴充鏈接池時用於獲取閒置鏈接總數,代碼以下:
public int getCount(final int state){
int count = 0;
for (IConcurrentBagEntry e : sharedList) {
if (e.getState() == state) {
count++;
}
}
return count;
}
以上就是ConcurrentBag的主要方法和處理鏈接對象的主要流程。
到這裏基本上一個鏈接的生產到獲取到回收到廢棄一整個生命週期在HikariCP內是如何管理的就說完了,相比以前的Druid的實現,有很大的不一樣,主要是HikariCP的無鎖獲取鏈接,本篇沒有涉及FastList的說明,由於從鏈接管理這個角度確實不多用到該結構,用到FastList的地方主要在存儲鏈接對象生成的statement對象以及用於存儲線程內緩存起來的鏈接對象;
除此以外HikariCP還利用javassist技術編譯期生成了ProxyConnection的初始化,這裏也沒有相關說明,網上有關HikariCP的優化有不少文章,大多數都提到了字節碼優化、fastList、concurrentBag的實現,本篇主要經過深刻解析HikariPool和ConcurrentBag的實現,來講明HikariCP相比Druid具體作了哪些不同的操做。