終於理解Spring Boot 爲何青睞HikariCP了,圖解的太透徹了!

 前言

如今已經有不少公司在使用HikariCP了,HikariCP還成爲了SpringBoot默認的鏈接池,伴隨着SpringBoot和微服務,HikariCP 必將迎來普遍的普及。mysql

下面博主帶你們從源碼角度分析一下HikariCP爲何可以被Spring Boot 青睞,文章目錄以下:sql

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

零、類圖和流程圖

開始前先來了解下HikariCP獲取一個鏈接時類間的交互流程,方便下面詳細流程的閱讀。網絡

獲取鏈接時的類間交互:多線程

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

1、主流程1:獲取鏈接流程

HikariCP獲取鏈接時的入口是HikariDataSource裏的getConnection方法,如今來看下該方法的具體流程:併發

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

上述爲HikariCP獲取鏈接時的流程圖,由圖1可知,每一個datasource對象裏都會持有一個HikariPool對象,記爲pool,初始化後的datasource對象pool是空的,因此第一次getConnection的時候會進行實例化pool屬性(參考主流程1),初始化的時候須要將當前datasource裏的config屬性傳過去,用於pool的初始化,最終標記sealed,而後根據pool對象調用getConnection方法(參考流程1.1),獲取成功後返回鏈接對象。異步

2、主流程2:初始化池對象

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

該流程用於初始化整個鏈接池,這個流程會給鏈接池內全部的屬性作初始化的工做,其中比較主要的幾個流程上圖已經指出,簡單歸納一下:socket

  1. 利用config初始化各類鏈接池屬性,而且產生一個用於生產物理鏈接的數據源DriverDataSource
  2. 初始化存放鏈接對象的核心類connectionBag
  3. 初始化一個延時任務線程池類型的對象houseKeepingExecutorService,用於後續執行一些延時/定時類任務(好比鏈接泄漏檢查延時任務,參考流程2.2以及主流程4,除此以外maxLifeTime後主動回收關閉鏈接也是交由該對象來執行的,這個過程能夠參考主流程3
  4. 預熱鏈接池,HikariCP會在該流程的checkFailFast裏初始化好一個鏈接對象放進池子內,固然觸發該流程得保證initializationTimeout > 0時(默認值1),這個配置屬性表示留給預熱操做的時間(默認值1在預熱失敗時不會發生重試)。與Druid經過initialSize控制預熱鏈接對象數不同的是,HikariCP僅預熱進池一個鏈接對象。
  5. 初始化一個線程池對象addConnectionExecutor,用於後續擴充鏈接對象
  6. 初始化一個線程池對象closeConnectionExecutor,用於關閉一些鏈接對象,怎麼觸發關閉任務呢?能夠參考流程1.1.2
3、流程1.1:經過HikariPool獲取鏈接對象

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

從最開始的結構圖可知,每一個HikariPool裏都維護一個ConcurrentBag對象,用於存放鏈接對象,由上圖能夠看到,實際上HikariPoolgetConnection就是從ConcurrentBag裏獲取鏈接的(調用其borrow方法得到,對應ConnectionBag主流程),在長鏈接檢查這塊,與以前說的Druid不一樣,這裏的長鏈接判活檢查在鏈接對象沒有被標記爲「已丟棄」時,只要距離上次使用超過500ms每次取出都會進行檢查(500ms是默認值,可經過配置com.zaxxer.hikari.aliveBypassWindowMs的系統參數來控制),emmmm,也就是說HikariCP對長鏈接的活性檢查很頻繁,可是其併發性能依舊優於Druid,說明頻繁的長鏈接檢查並非致使鏈接池性能高低的關鍵所在。ide

這個實際上是因爲HikariCP的無鎖實現,在高併發時對CPU的負載沒有其餘鏈接池那麼高而產生的併發性能差別,後面會說HikariCP的具體作法,即便是Druid,在獲取鏈接生成鏈接歸還鏈接時都進行了鎖控制,由於經過上篇解析Druid的文章能夠知道,Druid裏的鏈接池資源是多線程共享的,不可避免的會有鎖競爭,有鎖競爭意味着線程狀態的變化會很頻繁,線程狀態變化頻繁意味着CPU上下文切換也將會很頻繁。微服務

回到流程1.1,若是拿到的鏈接爲空,直接報錯,不爲空則進行相應的檢查,若是檢查經過,則包裝成ConnectionProxy對象返回給業務方,不經過則調用closeConnection方法關閉鏈接(對應流程1.1.2,該流程會觸發ConcurrentBagremove方法丟棄該鏈接,而後把實際的驅動鏈接交給closeConnectionExecutor線程池,異步關閉驅動鏈接)。高併發

4、流程1.1.1:鏈接判活

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

 承接上面的流程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屬性。

5、流程1.1.2:關閉鏈接對象

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

這個流程簡單來講就是把流程1.1.1中驗證不經過的死鏈接,主動關閉的一個流程,首先會把這個鏈接對象從ConnectionBag移除,而後把實際的物理鏈接交給一個線程池去異步執行,這個線程池就是在主流程2裏初始化池的時候初始化的線程池closeConnectionExecutor,而後異步任務內開始實際的關鏈接操做,由於主動關閉了一個鏈接至關於少了一個鏈接,因此還會觸發一次擴充鏈接池(參考主流程5)操做。

6、流程2.1:HikariCP監控設置

不一樣於Druid那樣監控指標那麼多,HikariCP會把咱們很是關心的幾項指標暴露給咱們,好比當前鏈接池內閒置鏈接數、總鏈接數、一個鏈接被用了多久歸還、建立一個物理鏈接花費多久等,HikariCP的鏈接池的監控咱們這一節專門詳細的分解一下,首先找到HikariCP下面的metrics文件夾,這下面放置了一些規範實現的監控接口等,還有一些現成的實現(好比HikariCP自帶對prometheusmicrometerdropwizard的支持,不太瞭解後面兩個,prometheus下文直接稱爲普羅米修斯):

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

下面,來着重看下接口的定義:

//這個接口的實現主要負責收集一些動做的耗時
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;
            }
        }
    }
}

實際上這裏就是這些屬性獲取和觸發刷新的地方,那麼這個對象是在哪裏被生成而且丟給MetricsTrackerFactorycreate方法的呢?這就是本節所須要講述的要點:主流程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的監控器就算是註冊進去了,因此要想實現本身的監控器拿到上面的指標,要通過以下步驟:

  1. 新建一個類實現IMetricsTracker接口,咱們這裏將該類記爲IMetricsTrackerImpl
  2. 新建一個類實現MetricsTrackerFactory接口,咱們這裏將該類記爲MetricsTrackerFactoryImpl,而且將上面的IMetricsTrackerImpl在其create方法內實例化
  3. 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原生的普羅米修斯收集器裏面對監控指標的命名並不符合我司的規範,因此就自定義了一個,有相似問題的不妨也試一試。

相關文章
相關標籤/搜索