SpringBoot官方為什么采用這個(gè)數(shù)據(jù)庫(kù)連接池字逗?史上最快函荣?

前言

現(xiàn)在已經(jīng)有很多公司在使用HikariCP了,HikariCP還成為了SpringBoot默認(rèn)的連接池扳肛,伴隨著SpringBoot和微服務(wù)傻挂,HikariCP 必將迎來廣泛的普及。

下面陳某帶大家從源碼角度分析一下HikariCP為什么能夠被Spring Boot 青睞挖息,文章目錄如下:

目錄

零金拒、類圖和流程圖

開始前先來了解下HikariCP獲取一個(gè)連接時(shí)類間的交互流程,方便下面詳細(xì)流程的閱讀套腹。

獲取連接時(shí)的類間交互:

圖1

一绪抛、主流程1:獲取連接流程

HikariCP獲取連接時(shí)的入口是HikariDataSource里的getConnection方法,現(xiàn)在來看下該方法的具體流程:

主流程

上述為HikariCP獲取連接時(shí)的流程圖电禀,由圖1可知幢码,每個(gè)datasource對(duì)象里都會(huì)持有一個(gè)HikariPool對(duì)象,記為pool尖飞,初始化后的datasource對(duì)象pool是空的症副,所以第一次getConnection的時(shí)候會(huì)進(jìn)行實(shí)例化pool屬性(參考主流程1),初始化的時(shí)候需要將當(dāng)前datasource里的config屬性傳過去政基,用于pool的初始化贞铣,最終標(biāo)記sealed,然后根據(jù)pool對(duì)象調(diào)用getConnection方法(參考流程1.1)沮明,獲取成功后返回連接對(duì)象辕坝。

二、主流程2:初始化池對(duì)象

主流程2

該流程用于初始化整個(gè)連接池荐健,這個(gè)流程會(huì)給連接池內(nèi)所有的屬性做初始化的工作酱畅,其中比較主要的幾個(gè)流程上圖已經(jīng)指出,簡(jiǎn)單概括一下:

  1. 利用config初始化各種連接池屬性江场,并且產(chǎn)生一個(gè)用于生產(chǎn)物理連接的數(shù)據(jù)源DriverDataSource
  2. 初始化存放連接對(duì)象的核心類connectionBag
  3. 初始化一個(gè)延時(shí)任務(wù)線程池類型的對(duì)象houseKeepingExecutorService纺酸,用于后續(xù)執(zhí)行一些延時(shí)/定時(shí)類任務(wù)(比如連接泄漏檢查延時(shí)任務(wù),參考流程2.2以及主流程4扛稽,除此之外maxLifeTime后主動(dòng)回收關(guān)閉連接也是交由該對(duì)象來執(zhí)行的吁峻,這個(gè)過程可以參考主流程3
  4. 預(yù)熱連接池,HikariCP會(huì)在該流程的checkFailFast里初始化好一個(gè)連接對(duì)象放進(jìn)池子內(nèi)在张,當(dāng)然觸發(fā)該流程得保證initializationTimeout > 0時(shí)(默認(rèn)值1)用含,這個(gè)配置屬性表示留給預(yù)熱操作的時(shí)間(默認(rèn)值1在預(yù)熱失敗時(shí)不會(huì)發(fā)生重試)。與Druid通過initialSize控制預(yù)熱連接對(duì)象數(shù)不一樣的是帮匾,HikariCP僅預(yù)熱進(jìn)池一個(gè)連接對(duì)象啄骇。
  5. 初始化一個(gè)線程池對(duì)象addConnectionExecutor,用于后續(xù)擴(kuò)充連接對(duì)象
  6. 初始化一個(gè)線程池對(duì)象closeConnectionExecutor瘟斜,用于關(guān)閉一些連接對(duì)象缸夹,怎么觸發(fā)關(guān)閉任務(wù)呢痪寻?可以參考流程1.1.2

三、流程1.1:通過HikariPool獲取連接對(duì)象

流程1.1

從最開始的結(jié)構(gòu)圖可知虽惭,每個(gè)HikariPool里都維護(hù)一個(gè)ConcurrentBag對(duì)象橡类,用于存放連接對(duì)象,由上圖可以看到芽唇,實(shí)際上HikariPoolgetConnection就是從ConcurrentBag里獲取連接的(調(diào)用其borrow方法獲得顾画,對(duì)應(yīng)ConnectionBag主流程),在長(zhǎng)連接檢查這塊匆笤,與之前說的Druid不同研侣,這里的長(zhǎng)連接判活檢查在連接對(duì)象沒有被標(biāo)記為“已丟棄”時(shí),只要距離上次使用超過500ms每次取出都會(huì)進(jìn)行檢查(500ms是默認(rèn)值炮捧,可通過配置com.zaxxer.hikari.aliveBypassWindowMs的系統(tǒng)參數(shù)來控制)庶诡,emmmm,也就是說HikariCP對(duì)長(zhǎng)連接的活性檢查很頻繁咆课,但是其并發(fā)性能依舊優(yōu)于Druid末誓,說明頻繁的長(zhǎng)連接檢查并不是導(dǎo)致連接池性能高低的關(guān)鍵所在。

這個(gè)其實(shí)是由于HikariCP的無鎖實(shí)現(xiàn)傀蚌,在高并發(fā)時(shí)對(duì)CPU的負(fù)載沒有其他連接池那么高而產(chǎn)生的并發(fā)性能差異基显,后面會(huì)說HikariCP的具體做法,即使是Druid善炫,在獲取連接生成連接库继、歸還連接時(shí)都進(jìn)行了鎖控制箩艺,因?yàn)橥ㄟ^上篇解析Druid的文章可以知道,Druid里的連接池資源是多線程共享的宪萄,不可避免的會(huì)有鎖競(jìng)爭(zhēng)艺谆,有鎖競(jìng)爭(zhēng)意味著線程狀態(tài)的變化會(huì)很頻繁,線程狀態(tài)變化頻繁意味著CPU上下文切換也將會(huì)很頻繁拜英。

回到流程1.1静汤,如果拿到的連接為空,直接報(bào)錯(cuò)居凶,不為空則進(jìn)行相應(yīng)的檢查虫给,如果檢查通過,則包裝成ConnectionProxy對(duì)象返回給業(yè)務(wù)方侠碧,不通過則調(diào)用closeConnection方法關(guān)閉連接(對(duì)應(yīng)流程1.1.2抹估,該流程會(huì)觸發(fā)ConcurrentBagremove方法丟棄該連接,然后把實(shí)際的驅(qū)動(dòng)連接交給closeConnectionExecutor線程池弄兜,異步關(guān)閉驅(qū)動(dòng)連接)药蜻。

四瓷式、流程1.1.1:連接判活

流程1.1.1

承接上面的流程1.1里的判活流程,來看下判活是如何做的语泽,首先說驗(yàn)證方法(注意這里該方法接受的這個(gè)connection對(duì)象不是poolEntry贸典,而是poolEntry持有的實(shí)際驅(qū)動(dòng)的連接對(duì)象),在之前介紹Druid的時(shí)候就知道踱卵,Druid是根據(jù)驅(qū)動(dòng)程序里是否存在ping方法來判斷是否啟用ping的方式判斷連接是否存活廊驼,但是到了HikariCP則更加簡(jiǎn)單粗暴,僅根據(jù)是否配置了connectionTestQuery覺定是否啟用ping:

this.isUseJdbc4Validation = config.getConnectionTestQuery() == null;

所以一般驅(qū)動(dòng)如果不是特別低的版本颊埃,不建議配置該項(xiàng)蔬充,否則便會(huì)走createStatement+excute的方式,相比ping簡(jiǎn)單發(fā)送心跳數(shù)據(jù)班利,這種方式顯然更低效饥漫。

此外,這里在剛進(jìn)來還會(huì)通過驅(qū)動(dòng)的連接對(duì)象重新給它設(shè)置一遍networkTimeout的值罗标,使之變成validationTimeout庸队,表示一次驗(yàn)證的超時(shí)時(shí)間,為啥這里要重新設(shè)置這個(gè)屬性呢闯割?因?yàn)樵谑褂胮ing方法校驗(yàn)時(shí)彻消,是沒辦法通過類似statement那樣可以setQueryTimeout的,所以只能由網(wǎng)絡(luò)通信的超時(shí)時(shí)間來控制宙拉,這個(gè)時(shí)間可以通過jdbc的連接參數(shù)socketTimeout來控制:

jdbc:mysql://127.0.0.1:3306/xxx?socketTimeout=250

這個(gè)值最終會(huì)被賦值給HikariCP的networkTimeout字段宾尚,這就是為什么最后那一步使用這個(gè)字段來還原驅(qū)動(dòng)連接超時(shí)屬性的原因;說到這里谢澈,最后那里為啥要再次還原呢煌贴?這就很容易理解了,因?yàn)轵?yàn)證結(jié)束了锥忿,連接對(duì)象還存活的情況下牛郑,它的networkTimeout的值這時(shí)仍然等于validationTimeout(不合預(yù)期),顯然在拿出去用之前敬鬓,需要恢復(fù)成本來的值淹朋,也就是HikariCP里的networkTimeout屬性。

五钉答、流程1.1.2:關(guān)閉連接對(duì)象

流程1.1.2

這個(gè)流程簡(jiǎn)單來說就是把流程1.1.1中驗(yàn)證不通過的死連接础芍,主動(dòng)關(guān)閉的一個(gè)流程,首先會(huì)把這個(gè)連接對(duì)象從ConnectionBag移除希痴,然后把實(shí)際的物理連接交給一個(gè)線程池去異步執(zhí)行者甲,這個(gè)線程池就是在主流程2里初始化池的時(shí)候初始化的線程池closeConnectionExecutor,然后異步任務(wù)內(nèi)開始實(shí)際的關(guān)連接操作砌创,因?yàn)橹鲃?dòng)關(guān)閉了一個(gè)連接相當(dāng)于少了一個(gè)連接虏缸,所以還會(huì)觸發(fā)一次擴(kuò)充連接池(參考主流程5)操作鲫懒。

六、流程2.1:HikariCP監(jiān)控設(shè)置

不同于Druid那樣監(jiān)控指標(biāo)那么多刽辙,HikariCP會(huì)把我們非常關(guān)心的幾項(xiàng)指標(biāo)暴露給我們窥岩,比如當(dāng)前連接池內(nèi)閑置連接數(shù)、總連接數(shù)宰缤、一個(gè)連接被用了多久歸還颂翼、創(chuàng)建一個(gè)物理連接花費(fèi)多久等,HikariCP的連接池的監(jiān)控我們這一節(jié)專門詳細(xì)的分解一下慨灭,首先找到HikariCP下面的metrics文件夾朦乏,這下面放置了一些規(guī)范實(shí)現(xiàn)的監(jiān)控接口等,還有一些現(xiàn)成的實(shí)現(xiàn)(比如HikariCP自帶對(duì)prometheus氧骤、micrometer呻疹、dropwizard的支持,不太了解后面兩個(gè)筹陵,prometheus下文直接稱為普羅米修斯):

圖2

下面刽锤,來著重看下接口的定義:

//這個(gè)接口的實(shí)現(xiàn)主要負(fù)責(zé)收集一些動(dòng)作的耗時(shí)
public interface IMetricsTracker extends AutoCloseable {
    //這個(gè)方法觸發(fā)點(diǎn)在創(chuàng)建實(shí)際的物理連接時(shí)(主流程3),用于記錄一個(gè)實(shí)際的物理連接創(chuàng)建所耗費(fèi)的時(shí)間
    default void recordConnectionCreatedMillis(long connectionCreatedMillis) {}

    //這個(gè)方法觸發(fā)點(diǎn)在getConnection時(shí)(主流程1)朦佩,用于記錄獲取一個(gè)連接時(shí)實(shí)際的耗時(shí)
    default void recordConnectionAcquiredNanos(final long elapsedAcquiredNanos) {}

    //這個(gè)方法觸發(fā)點(diǎn)在回收連接時(shí)(主流程6)并思,用于記錄一個(gè)連接從被獲取到被回收時(shí)所消耗的時(shí)間
    default void recordConnectionUsageMillis(final long elapsedBorrowedMillis) {}

    //這個(gè)方法觸發(fā)點(diǎn)也在getConnection時(shí)(主流程1),用于記錄獲取連接超時(shí)的次數(shù)语稠,每發(fā)生一次獲取連接超時(shí)宋彼,就會(huì)觸發(fā)一次該方法的調(diào)用
    default void recordConnectionTimeout() {}

    @Override
    default void close() {}
}

觸發(fā)點(diǎn)都了解清楚后,再來看看MetricsTrackerFactory的接口定義:

//用于創(chuàng)建IMetricsTracker實(shí)例仙畦,并且按需記錄PoolStats對(duì)象里的屬性(這個(gè)對(duì)象里的屬性就是類似連接池當(dāng)前閑置連接數(shù)之類的線程池狀態(tài)類指標(biāo))
public interface MetricsTrackerFactory {
    //返回一個(gè)IMetricsTracker對(duì)象宙暇,并且把PoolStats傳了過去
    IMetricsTracker create(String poolName, PoolStats poolStats);
}

上面的接口用法見注釋,針對(duì)新出現(xiàn)的PoolStats類议泵,我們來看看它做了什么:

public abstract class PoolStats {
    private final AtomicLong reloadAt; //觸發(fā)下次刷新的時(shí)間(時(shí)間戳)
    private final long timeoutMs; //刷新下面的各項(xiàng)屬性值的頻率,默認(rèn)1s桃熄,無法改變

    // 總連接數(shù)
    protected volatile int totalConnections;
    // 閑置連接數(shù)
    protected volatile int idleConnections;
    // 活動(dòng)連接數(shù)
    protected volatile int activeConnections;
    // 由于無法獲取到可用連接而阻塞的業(yè)務(wù)線程數(shù)
    protected volatile int pendingThreads;
    // 最大連接數(shù)
    protected volatile int maxConnections;
    // 最小連接數(shù)
    protected volatile int minConnections;

    public PoolStats(final long timeoutMs) {
        this.timeoutMs = timeoutMs;
        this.reloadAt = new AtomicLong();
    }

    //這里以獲取最大連接數(shù)為例先口,其他的跟這個(gè)差不多
    public int getMaxConnections() {
        if (shouldLoad()) { //是否應(yīng)該刷新
            update(); //刷新屬性值,注意這個(gè)update的實(shí)現(xiàn)在HikariPool里瞳收,因?yàn)檫@些屬性值的直接或間接來源都是HikariPool
        }

        return maxConnections;
    }

    protected abstract void update(); //實(shí)現(xiàn)在↑上面已經(jīng)說了

    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;
            }
        }
    }
}

實(shí)際上這里就是這些屬性獲取和觸發(fā)刷新的地方碉京,那么這個(gè)對(duì)象是在哪里被生成并且丟給MetricsTrackerFactorycreate方法的呢?這就是本節(jié)所需要講述的要點(diǎn):主流程2里的設(shè)置監(jiān)控器的流程螟深,來看看那里發(fā)生了什么事吧:

//監(jiān)控器設(shè)置方法(此方法在HikariPool中谐宙,metricsTracker屬性就是HikariPool用來觸發(fā)IMetricsTracker里方法調(diào)用的)
public void setMetricsTrackerFactory(MetricsTrackerFactory metricsTrackerFactory) {
    if (metricsTrackerFactory != null) {
        //MetricsTrackerDelegate是包裝類,是HikariPool的一個(gè)靜態(tài)內(nèi)部類界弧,是實(shí)際持有IMetricsTracker對(duì)象的類凡蜻,也是實(shí)際觸發(fā)IMetricsTracker里方法調(diào)用的類
        //這里首先會(huì)觸發(fā)MetricsTrackerFactory類的create方法拿到IMetricsTracker對(duì)象搭综,然后利用getPoolStats初始化PoolStat對(duì)象,然后也一并傳給MetricsTrackerFactory
        this.metricsTracker = new MetricsTrackerDelegate(metricsTrackerFactory.create(config.getPoolName(), getPoolStats()));
    } else {
        //不啟用監(jiān)控划栓,直接等于一個(gè)沒有實(shí)現(xiàn)方法的空類
        this.metricsTracker = new NopMetricsTrackerDelegate();
    }
}

private PoolStats getPoolStats() {
    //初始化PoolStats對(duì)象兑巾,并且規(guī)定1s觸發(fā)一次屬性值刷新的update方法
    return new PoolStats(SECONDS.toMillis(1)) {
        @Override
        protected void update() {
            //實(shí)現(xiàn)了PoolStat的update方法,刷新各個(gè)屬性的值
            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的監(jiān)控器就算是注冊(cè)進(jìn)去了忠荞,所以要想實(shí)現(xiàn)自己的監(jiān)控器拿到上面的指標(biāo)蒋歌,要經(jīng)過如下步驟:

  1. 新建一個(gè)類實(shí)現(xiàn)IMetricsTracker接口,我們這里將該類記為IMetricsTrackerImpl
  2. 新建一個(gè)類實(shí)現(xiàn)MetricsTrackerFactory接口委煤,我們這里將該類記為MetricsTrackerFactoryImpl堂油,并且將上面的IMetricsTrackerImpl在其create方法內(nèi)實(shí)例化
  3. MetricsTrackerFactoryImpl實(shí)例化后調(diào)用HikariPool的setMetricsTrackerFactory方法注冊(cè)到Hikari連接池。

上面沒有提到PoolStats里的屬性怎么監(jiān)控碧绞,這里來說下府框,由于create方法是調(diào)用一次就沒了,create方法只是接收了PoolStats對(duì)象的實(shí)例头遭,如果不處理寓免,那么隨著create調(diào)用的結(jié)束,這個(gè)實(shí)例針對(duì)監(jiān)控模塊來說就失去持有了计维,所以這里如果想要拿到PoolStats里的屬性袜香,就需要開啟一個(gè)守護(hù)線程,讓其持有PoolStats對(duì)象實(shí)例鲫惶,并且定時(shí)獲取其內(nèi)部屬性值蜈首,然后push給監(jiān)控系統(tǒng),如果是普羅米修斯等使用pull方式獲取監(jiān)控?cái)?shù)據(jù)的監(jiān)控系統(tǒng)欠母,可以效仿HikariCP原生普羅米修斯監(jiān)控的實(shí)現(xiàn)欢策,自定義一個(gè)Collector對(duì)象來接收PoolStats實(shí)例,這樣普羅米修斯就可以定期拉取了赏淌,比如HikariCP根據(jù)普羅米修斯監(jiān)控系統(tǒng)自己定義的MetricsTrackerFactory實(shí)現(xiàn)(對(duì)應(yīng)圖2里的PrometheusMetricsTrackerFactory類):

@Override
public IMetricsTracker create(String poolName, PoolStats poolStats) {
    getCollector().add(poolName, poolStats); //將接收到的PoolStats對(duì)象直接交給Collector踩寇,這樣普羅米修斯服務(wù)端每觸發(fā)一次采集接口的調(diào)用,PoolStats都會(huì)跟著執(zhí)行一遍內(nèi)部屬性獲取流程
    return new PrometheusMetricsTracker(poolName, this.collectorRegistry); //返回IMetricsTracker接口的實(shí)現(xiàn)類
}

//自定義的Collector
private HikariCPCollector getCollector() {
    if (collector == null) {
        //注冊(cè)到普羅米修斯收集中心
        collector = new HikariCPCollector().register(this.collectorRegistry);
    }
    return collector;

通過上面的解釋可以知道在HikariCP中如何自定義一個(gè)自己的監(jiān)控器六水,以及相比Druid的監(jiān)控俺孙,有什么區(qū)別。 工作中很多時(shí)候都是需要自定義的掷贾,我司雖然也是用的普羅米修斯監(jiān)控睛榄,但是因?yàn)镠ikariCP原生的普羅米修斯收集器里面對(duì)監(jiān)控指標(biāo)的命名并不符合我司的規(guī)范,所以就自定義了一個(gè)想帅,有類似問題的不妨也試一試场靴。

?? 這一節(jié)沒有畫圖,純代碼,因?yàn)楫媹D不太好解釋這部分的東西旨剥,這部分內(nèi)容與連接池整體流程關(guān)系也不大咧欣,充其量獲取了連接池本身的一些屬性,在連接池里的觸發(fā)點(diǎn)也在上面代碼段的注釋里說清楚了泞边,看代碼定義可能更好理解一些该押。

七、流程2.2:連接泄漏的檢測(cè)與告警

本節(jié)對(duì)應(yīng)主流程2里的子流程2.2阵谚,在初始化池對(duì)象時(shí)蚕礼,初始化了一個(gè)叫做leakTaskFactory的屬性,本節(jié)來看下它具體是用來做什么的梢什。

7.1:它是做什么的奠蹬?

一個(gè)連接被拿出去使用時(shí)間超過leakDetectionThreshold(可配置,默認(rèn)0)未歸還的嗡午,會(huì)觸發(fā)一個(gè)連接泄漏警告囤躁,通知業(yè)務(wù)方目前存在連接泄漏的問題。

7.2:過程詳解

該屬性是ProxyLeakTaskFactory類型對(duì)象荔睹,且它還會(huì)持有houseKeepingExecutorService這個(gè)線程池對(duì)象狸演,用于生產(chǎn)ProxyLeakTask對(duì)象,然后利用上面的houseKeepingExecutorService延時(shí)運(yùn)行該對(duì)象里的run方法僻他。該流程的觸發(fā)點(diǎn)在上面的流程1.1最后包裝成ProxyConnection對(duì)象的那一步宵距,來看看具體的流程圖:

流程2.2

每次在流程1.1那里生成ProxyConnection對(duì)象時(shí),都會(huì)觸發(fā)上面的流程吨拗,由流程圖可以知道满哪,ProxyConnection對(duì)象持有PoolEntryProxyLeakTask的對(duì)象,其中初始化ProxyLeakTask對(duì)象時(shí)就用到了leakTaskFactory對(duì)象劝篷,通過其schedule方法可以進(jìn)行ProxyLeakTask的初始化哨鸭,并將其實(shí)例傳遞給ProxyConnection進(jìn)行初始化賦值(ps:由圖知ProxyConnection在觸發(fā)回收事件時(shí),會(huì)主動(dòng)取消這個(gè)泄漏檢查任務(wù)娇妓,這也是ProxyConnection需要持有ProxyLeakTask對(duì)象的原因)像鸡。

在上面的流程圖中可以知道,只有在leakDetectionThreshold不等于0的時(shí)候才會(huì)生成一個(gè)帶有實(shí)際延時(shí)任務(wù)的ProxyLeakTask對(duì)象哈恰,否則返回?zé)o實(shí)際意義的空對(duì)象坟桅。所以要想啟用連接泄漏檢查,首先要把leakDetectionThreshold配置設(shè)置上蕊蝗,這個(gè)屬性表示經(jīng)過該時(shí)間后借出去的連接仍未歸還,則觸發(fā)連接泄漏告警赖舟。

ProxyConnection之所以要持有ProxyLeakTask對(duì)象蓬戚,是因?yàn)樗梢员O(jiān)聽到連接是否觸發(fā)歸還操作,如果觸發(fā)宾抓,則調(diào)用cancel方法取消延時(shí)任務(wù)子漩,防止誤告豫喧。

由此流程可以知道,跟Druid一樣幢泼,HikariCP也有連接對(duì)象泄漏檢查紧显,與Druid主動(dòng)回收連接相比,HikariCP實(shí)現(xiàn)更加簡(jiǎn)單缕棵,僅僅是在觸發(fā)時(shí)打印警告日志孵班,不會(huì)采取具體的強(qiáng)制回收的措施。

與Druid一樣招驴,默認(rèn)也是關(guān)閉這個(gè)流程的篙程,因?yàn)閷?shí)際開發(fā)中一般使用第三方框架,框架本身會(huì)保證及時(shí)的close連接别厘,防止連接對(duì)象泄漏虱饿,開啟與否還是取決于業(yè)務(wù)是否需要,如果一定要開啟触趴,如何設(shè)置leakDetectionThreshold的大小也是需要考慮的一件事氮发。

八、主流程3:生成連接對(duì)象

本節(jié)來講下主流程2里的createEntry方法冗懦,這個(gè)方法利用PoolBase里的DriverDataSource對(duì)象生成一個(gè)實(shí)際的連接對(duì)象(如果忘記DriverDatasource是哪里初始化的了爽冕,可以看下主流程2PoolBaseinitializeDataSource方法的作用),然后用PoolEntry類包裝成PoolEntry對(duì)象批狐,現(xiàn)在來看下這個(gè)包裝類有哪些主要屬性:

final class PoolEntry implements IConcurrentBagEntry {
    private static final Logger LOGGER = LoggerFactory.getLogger(PoolEntry.class);
    //通過cas來修改state屬性
    private static final AtomicIntegerFieldUpdater stateUpdater;

    Connection connection; //實(shí)際的物理連接對(duì)象
    long lastAccessed; //觸發(fā)回收時(shí)刷新該時(shí)間扇售,表示“最近一次使用時(shí)間”
    long lastBorrowed; //getConnection里borrow成功后刷新該時(shí)間,表示“最近一次借出的時(shí)間”

    @SuppressWarnings("FieldCanBeLocal")
    private volatile int state = 0; //連接狀態(tài)嚣艇,枚舉值:IN_USE(使用中)承冰、NOT_IN_USE(閑置中)、REMOVED(已移除)食零、RESERVED(標(biāo)記為保留中)
    private volatile boolean evict; //是否被標(biāo)記為廢棄困乒,很多地方用到(比如流程1.1靠這個(gè)判斷連接是否已被廢棄,再比如主流程4里時(shí)鐘回?fù)軙r(shí)觸發(fā)的直接廢棄邏輯)

    private volatile ScheduledFuture<?> endOfLife; //用于在超過連接生命周期(maxLifeTime)時(shí)廢棄連接的延時(shí)任務(wù)贰谣,這里poolEntry要持有該對(duì)象娜搂,主要是因?yàn)樵趯?duì)象主動(dòng)被關(guān)閉時(shí)(意味著不需要在超過maxLifeTime時(shí)主動(dòng)失效了),需要cancel掉該任務(wù)

    private final FastList openStatements; //當(dāng)前該連接對(duì)象上生成的所有的statement對(duì)象吱抚,用于在回收連接時(shí)主動(dòng)關(guān)閉這些對(duì)象百宇,防止存在漏關(guān)的statement
    private final HikariPool hikariPool; //持有pool對(duì)象

    private final boolean isReadOnly; //是否為只讀
    private final boolean isAutoCommit; //是否存在事務(wù)
}

上面就是整個(gè)PoolEntry對(duì)象里所有的屬性,這里再說下endOfLife對(duì)象秘豹,它是一個(gè)利用houseKeepingExecutorService這個(gè)線程池對(duì)象做的延時(shí)任務(wù)携御,這個(gè)延時(shí)任務(wù)一般在創(chuàng)建好連接對(duì)象后maxLifeTime左右的時(shí)間觸發(fā),具體來看下createEntry代碼:

private PoolEntry createPoolEntry() {

        final PoolEntry poolEntry = newPoolEntry(); //生成實(shí)際的連接對(duì)象

        final long maxLifetime = config.getMaxLifetime(); //拿到配置好的maxLifetime
        if (maxLifetime > 0) { //<=0的時(shí)候不啟用主動(dòng)過期策略
            // 計(jì)算需要減去的隨機(jī)數(shù)
            // 源注釋: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; //生成實(shí)際的延時(shí)時(shí)間
            poolEntry.setFutureEol(houseKeepingExecutorService.schedule(
                    () -> { //實(shí)際的延時(shí)任務(wù),這里直接觸發(fā)softEvictConnection啄刹,而softEvictConnection內(nèi)則會(huì)標(biāo)記該連接對(duì)象為廢棄狀態(tài)涮坐,然后嘗試修改其狀態(tài)為STATE_RESERVED,若成功誓军,則觸發(fā)closeConnection(對(duì)應(yīng)流程1.1.2)
                        if (softEvictConnection(poolEntry, "(connection has passed maxLifetime)", false /* not owner */)) {
                            addBagItem(connectionBag.getWaitingThreadCount()); //回收完畢后袱讹,連接池內(nèi)少了一個(gè)連接,就會(huì)嘗試新增一個(gè)連接對(duì)象
                        }
                    },
                    lifetime, MILLISECONDS)); //給endOfLife賦值昵时,并且提交延時(shí)任務(wù)捷雕,lifetime后觸發(fā)
        }

        return poolEntry;
    }

    //觸發(fā)新增連接任務(wù)
    public void addBagItem(final int waiting) {
        //前排提示:addConnectionQueue和addConnectionExecutor的關(guān)系和初始化參考主流程2

        //當(dāng)添加連接的隊(duì)列里已提交的任務(wù)超過那些因?yàn)楂@取不到連接而發(fā)生阻塞的線程個(gè)數(shù)時(shí),就進(jìn)行提交連接新增連接的任務(wù)
        final boolean shouldAdd = waiting - addConnectionQueue.size() >= 0; // Yes, >= is intentional.
        if (shouldAdd) {
            //提交任務(wù)給addConnectionExecutor這個(gè)線程池债查,PoolEntryCreator是一個(gè)實(shí)現(xiàn)了Callable接口的類非区,下面將通過流程圖的方式介紹該類的call方法
            addConnectionExecutor.submit(poolEntryCreator);
        }
    }

通過上面的流程,可以知道盹廷,HikariCP一般通過createEntry方法來新增一個(gè)連接入池征绸,每個(gè)連接被包裝成PoolEntry對(duì)象,在創(chuàng)建好對(duì)象時(shí)俄占,同時(shí)會(huì)提交一個(gè)延時(shí)任務(wù)來關(guān)閉廢棄該連接管怠,這個(gè)時(shí)間就是我們配置的maxLifeTime,為了保證不在同一時(shí)間失效缸榄,HikariCP還會(huì)利用maxLifeTime減去一個(gè)隨機(jī)數(shù)作為最終的延時(shí)任務(wù)延遲時(shí)間渤弛,然后在觸發(fā)廢棄任務(wù)時(shí)葫男,還會(huì)觸發(fā)addBagItem隧出,進(jìn)行連接添加任務(wù)(因?yàn)閺U棄了一個(gè)連接晓避,需要往池子里補(bǔ)充一個(gè))尘盼,該任務(wù)則交給由主流程2里定義好的addConnectionExecutor線程池執(zhí)行,那么寓辱,現(xiàn)在來看下這個(gè)異步添加連接對(duì)象的任務(wù)流程:

addConnectionExecutor的call流程

這個(gè)流程就是往連接池里加連接用的压状,跟createEntry結(jié)合起來說是因?yàn)檫@倆流程是緊密相關(guān)的魂贬,除此之外碉输,主流程5fillPool籽前,擴(kuò)充連接池)也會(huì)觸發(fā)該任務(wù)。

九敷钾、主流程4:連接池縮容

HikariCP會(huì)按照minIdle定時(shí)清理閑置過久的連接枝哄,這個(gè)定時(shí)任務(wù)在主流程2初始化連接池對(duì)象時(shí)被啟用,跟上面的流程一樣阻荒,也是利用houseKeepingExecutorService這個(gè)線程池對(duì)象做該定時(shí)任務(wù)的執(zhí)行器挠锥。

來看下主流程2里是怎么啟用該任務(wù)的:

//housekeepingPeriodMs的默認(rèn)值是30s,所以定時(shí)任務(wù)的間隔為30s
this.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, housekeepingPeriodMs, MILLISECONDS);

那么本節(jié)主要來說下HouseKeeper這個(gè)類侨赡,該類實(shí)現(xiàn)了Runnable接口瘪贱,回收邏輯主要在其run方法內(nèi)纱控,來看看run方法的邏輯流程圖:

主流程4:連接池縮容

上面的流程就是HouseKeeper的run方法里具體做的事情,由于系統(tǒng)時(shí)間回?fù)軙?huì)導(dǎo)致該定時(shí)任務(wù)回收一些連接時(shí)產(chǎn)生誤差菜秦,因此存在如下判斷:

//now就是當(dāng)前系統(tǒng)時(shí)間,previous就是上次觸發(fā)該任務(wù)時(shí)的時(shí)間舶掖,housekeepingPeriodMs就是隔多久觸發(fā)該任務(wù)一次
//也就是說plusMillis(previous, housekeepingPeriodMs)表示當(dāng)前時(shí)間
//如果系統(tǒng)時(shí)間沒被回?fù)芮蜃颍敲磒lusMillis(now, 128)一定是大于當(dāng)前時(shí)間的,如果被系統(tǒng)時(shí)間被回?fù)?//回?fù)艿臅r(shí)間超過128ms眨攘,那么下面的判斷就成立主慰,否則永遠(yuǎn)不會(huì)成立
if (plusMillis(now, 128) < plusMillis(previous, housekeepingPeriodMs))

這是hikariCP在解決系統(tǒng)時(shí)鐘被回?fù)軙r(shí)做出的一種措施,通過流程圖可以看到鲫售,它是直接把池子里所有的連接對(duì)象取出來挨個(gè)兒的標(biāo)記成廢棄共螺,并且嘗試把狀態(tài)值修改為STATE_RESERVED(后面會(huì)說明這些狀態(tài),這里先不深究)情竹。如果系統(tǒng)時(shí)鐘沒有發(fā)生改變(絕大多數(shù)情況會(huì)命中這一塊的邏輯)藐不,由圖知,會(huì)把當(dāng)前池內(nèi)所有處于閑置狀態(tài)(STATE_NOT_IN_USE)的連接拿出來秦效,然后計(jì)算需要檢查的范圍雏蛮,然后循環(huán)著修改連接的狀態(tài):

//拿到所有處于閑置狀態(tài)的連接
final List notInUse = connectionBag.values(STATE_NOT_IN_USE);
//計(jì)算出需要被檢查閑置時(shí)間的數(shù)量,簡(jiǎn)單來說阱州,池內(nèi)需要保證最小minIdle個(gè)連接活著挑秉,所以需要計(jì)算出超出這個(gè)范圍的閑置對(duì)象進(jìn)行檢查
int toRemove = notInUse.size() - config.getMinIdle();
for (PoolEntry entry : notInUse) {
  //在檢查范圍內(nèi),且閑置時(shí)間超出idleTimeout苔货,然后嘗試將連接對(duì)象狀態(tài)由STATE_NOT_IN_USE變?yōu)镾TATE_RESERVED成功
  if (toRemove > 0 && elapsedMillis(entry.lastAccessed, now) > idleTimeout && connectionBag.reserve(entry)) {
    closeConnection(entry, "(connection has passed idleTimeout)"); //滿足上述條件犀概,進(jìn)行連接關(guān)閉
    toRemove--;
  }
}
fillPool(); //因?yàn)榭赡芑厥樟艘恍┻B接,所以要再次觸發(fā)連接池?cái)U(kuò)充流程檢查下是否需要新增連接夜惭。

上面的代碼就是流程圖里對(duì)應(yīng)的沒有回?fù)芟到y(tǒng)時(shí)間時(shí)的流程邏輯姻灶。該流程在idleTimeout大于0(默認(rèn)等于0)并且minIdle小于maxPoolSize的時(shí)候才會(huì)啟用,默認(rèn)是不啟用的滥嘴,若需要啟用木蹬,可以按照條件來配置。

十若皱、主流程5:擴(kuò)充連接池

這個(gè)流程主要依附HikariPool里的fillPool方法镊叁,這個(gè)方法已經(jīng)在上面很多流程里出現(xiàn)過了,它的作用就是在觸發(fā)連接廢棄走触、連接池連接不夠用時(shí)晦譬,發(fā)起擴(kuò)充連接數(shù)的操作,這是個(gè)很簡(jiǎn)單的過程互广,下面看下源碼(為了使代碼結(jié)構(gòu)更加清晰敛腌,對(duì)源碼做了細(xì)微改動(dòng)):

// PoolEntryCreator關(guān)于call方法的實(shí)現(xiàn)流程在主流程3里已經(jīng)看過了卧土,但是這里卻有倆PoolEntryCreator對(duì)象,
// 這是個(gè)較細(xì)節(jié)的地方像樊,用于打日志用尤莺,不再說這部分,為了便于理解生棍,只需要知道這倆對(duì)象執(zhí)行的是同一塊call方法即可
private final PoolEntryCreator poolEntryCreator = new PoolEntryCreator(null);
private final PoolEntryCreator postFillPoolEntryCreator = new PoolEntryCreator("After adding ");

private synchronized void fillPool() {
  // 這個(gè)判斷就是根據(jù)當(dāng)前池子里相關(guān)數(shù)據(jù)颤霎,推算出需要擴(kuò)充的連接數(shù),
  // 判斷方式就是利用最大連接數(shù)跟當(dāng)前連接總數(shù)的差值涂滴,與最小連接數(shù)與當(dāng)前池內(nèi)閑置的連接數(shù)的差值友酱,取其最小的那一個(gè)得到
  int needAdd = Math.min(maxPoolSize - connectionBag.size(),
  minIdle - connectionBag.getCount(STATE_NOT_IN_USE));

  //減去當(dāng)前排隊(duì)的任務(wù),就是最終需要新增的連接數(shù)
  final int connectionsToAdd = needAdd - addConnectionQueue.size();
  for (int i = 0; i < connectionsToAdd; i++) {
    //一般循環(huán)的最后一次會(huì)命中postFillPoolEntryCreator任務(wù)柔纵,其實(shí)就是在最后一次會(huì)打印一次日志而已(可以忽略該干擾邏輯)
    addConnectionExecutor.submit((i < connectionsToAdd - 1) ? poolEntryCreator : postFillPoolEntryCreator);
  }
}

由該過程可以知道缔杉,最終這個(gè)新增連接的任務(wù)也是交由addConnectionExecutor線程池來處理的,而任務(wù)的主題也是PoolEntryCreator搁料,這個(gè)流程可以參考主流程3.

然后needAdd的推算:

Math.min(最大連接數(shù) - 池內(nèi)當(dāng)前連接總數(shù), 最小連接數(shù) - 池內(nèi)閑置的連接數(shù))

根據(jù)這個(gè)方式判斷或详,可以保證池內(nèi)的連接數(shù)永遠(yuǎn)不會(huì)超過maxPoolSize,也永遠(yuǎn)不會(huì)低于minIdle加缘。在連接吃緊的時(shí)候鸭叙,可以保證每次觸發(fā)都以minIdle的數(shù)量擴(kuò)容。因此如果在maxPoolSizeminIdle配置的值一樣的話拣宏,在池內(nèi)連接吃緊的時(shí)候沈贝,就不會(huì)發(fā)生任何擴(kuò)容了。

十一勋乾、主流程6:連接回收

最開始說過宋下,最終真實(shí)的物理連接對(duì)象會(huì)被包裝成PoolEntry對(duì)象,存放進(jìn)ConcurrentBag辑莫,然后獲取時(shí)学歧,PoolEntry對(duì)象又會(huì)被再次包裝成ProxyConnection對(duì)象暴露給使用方的,那么觸發(fā)連接回收各吨,實(shí)際上就是觸發(fā)ProxyConnection里的close方法:

public final void close() throws SQLException {
  // 原注釋:Closing statements can cause connection eviction, so this must run before the conditional below
  closeStatements(); //此連接對(duì)象在業(yè)務(wù)方使用過程中產(chǎn)生的所有statement對(duì)象枝笨,進(jìn)行統(tǒng)一close,防止漏close的情況
  if (delegate != ClosedConnection.CLOSED_CONNECTION) {
    leakTask.cancel(); //取消連接泄漏檢查任務(wù)揭蜒,參考流程2.2
    try {
      if (isCommitStateDirty && !isAutoCommit) { //在存在執(zhí)行語句后并且還打開了事務(wù)横浑,調(diào)用close時(shí)需要主動(dòng)回滾事務(wù)
        delegate.rollback(); //回滾
        lastAccess = currentTime(); //刷新"最后一次使用時(shí)間"
      }
    } finally {
      delegate = ClosedConnection.CLOSED_CONNECTION;
      poolEntry.recycle(lastAccess); //觸發(fā)回收
    }
  }
}

這個(gè)就是ProxyConnection里的close方法,可以看到它最終會(huì)調(diào)用PoolEntry的recycle方法進(jìn)行回收屉更,除此之外徙融,連接對(duì)象的最后一次使用時(shí)間也是在這個(gè)時(shí)候刷新的,該時(shí)間是個(gè)很重要的屬性瑰谜,可以用來判斷一個(gè)連接對(duì)象的閑置時(shí)間欺冀,來看下PoolEntry的recycle方法:

void recycle(final long lastAccessed) {
  if (connection != null) {
    this.lastAccessed = lastAccessed; //刷新最后使用時(shí)間
    hikariPool.recycle(this); //觸發(fā)HikariPool的回收方法树绩,把自己傳過去
  }
}

之前有說過,每個(gè)PoolEntry對(duì)象都持有HikariPool的對(duì)象隐轩,方便觸發(fā)連接池的一些操作饺饭,由上述代碼可以看到,最終還是會(huì)觸發(fā)HikariPool里的recycle方法职车,再來看下HikariPool的recycle方法:

void recycle(final PoolEntry poolEntry) {
  metricsTracker.recordConnectionUsage(poolEntry); //監(jiān)控指標(biāo)相關(guān)砰奕,忽略
  connectionBag.requite(poolEntry); //最終觸發(fā)connectionBag的requite方法歸還連接,該流程參考ConnectionBag主流程里的requite方法部分
}

以上就是連接回收部分的邏輯提鸟,相比其他流程,還是比較簡(jiǎn)潔的仅淑。

十二称勋、ConcurrentBag主流程

這個(gè)類用來存放最終的PoolEntry類型的連接對(duì)象,提供了基本的增刪查的功能涯竟,被HikariPool持有赡鲜,上面那么多的操作,幾乎都是在HikariPool中完成的庐船,HikariPool用來管理實(shí)際的連接生產(chǎn)動(dòng)作和回收動(dòng)作银酬,實(shí)際操作的卻是ConcurrentBag類,梳理下上面所有流程的觸發(fā)點(diǎn):

  • 主流程2:初始化HikariPool時(shí)初始化ConcurrentBag(構(gòu)造方法)筐钟,預(yù)熱時(shí)通過createEntry拿到連接對(duì)象揩瞪,調(diào)用ConcurrentBag.add添加連接到ConcurrentBag。
  • 流程1.1:通過HikariPool獲取連接時(shí)篓冲,通過調(diào)用ConcurrentBag.borrow拿到一個(gè)連接對(duì)象李破。
  • 主流程6:通過ConcurrentBag.requite歸還一個(gè)連接。
  • 流程1.1.2:觸發(fā)關(guān)閉連接時(shí)壹将,會(huì)通過ConcurrentBag.remove移除連接對(duì)象嗤攻,由前面的流程可知關(guān)閉連接觸發(fā)點(diǎn)為:連接超過最大生命周期maxLifeTime主動(dòng)廢棄、健康檢查不通過主動(dòng)廢棄诽俯、連接池縮容妇菱。
  • 主流程3:通過異步添加連接時(shí),通過調(diào)用ConcurrentBag.add添加連接到ConcurrentBag暴区,由前面的流程可知添加連接觸發(fā)點(diǎn)為:連接超過最大生命周期maxLifeTime主動(dòng)廢棄連接后闯团、連接池?cái)U(kuò)容。
  • 主流程4:連接池縮容任務(wù)颜启,通過調(diào)用ConcurrentBag.values篩選出需要的做操作的連接對(duì)象偷俭,然后再通過ConcurrentBag.reserve完成對(duì)連接對(duì)象狀態(tài)的修改,然后會(huì)通過流程1.1.2觸發(fā)關(guān)閉和移除連接操作缰盏。

通過觸發(fā)點(diǎn)整理涌萤,可以知道該結(jié)構(gòu)里的主要方法淹遵,就是上面觸發(fā)點(diǎn)里標(biāo)記為標(biāo)簽色的部分,然后來具體看下該類的基本定義和主要方法:

public class ConcurrentBag<T extends IConcurrentBagEntry> implements AutoCloseable {

    private final CopyOnWriteArrayList<T> sharedList; //最終存放PoolEntry對(duì)象的地方负溪,它是一個(gè)CopyOnWriteArrayList
    private final boolean weakThreadLocals; //默認(rèn)false透揣,為true時(shí)可以讓一個(gè)連接對(duì)象在下方threadList里的list內(nèi)處于弱引用狀態(tài),防止內(nèi)存泄漏(參見備注1)

    private final ThreadLocal<List<Object>> threadList; //線程級(jí)的緩存川抡,從sharedList拿到的連接對(duì)象辐真,會(huì)被緩存進(jìn)當(dāng)前線程內(nèi),borrow時(shí)會(huì)先從緩存中拿崖堤,從而達(dá)到池內(nèi)無鎖實(shí)現(xiàn)
    private final IBagStateListener listener; //內(nèi)部接口侍咱,HikariPool實(shí)現(xiàn)了該接口,主要用于ConcurrentBag主動(dòng)通知HikariPool觸發(fā)添加連接對(duì)象的異步操作(也就是主流程3里的addConnectionExecutor所觸發(fā)的流程)
    private final AtomicInteger waiters; //當(dāng)前因?yàn)楂@取不到連接而發(fā)生阻塞的業(yè)務(wù)線程數(shù)密幔,這個(gè)在之前的流程里也出現(xiàn)過楔脯,比如主流程3里addBagItem就會(huì)根據(jù)該指標(biāo)進(jìn)行判斷是否需要新增連接
    private volatile boolean closed; //標(biāo)記當(dāng)前ConcurrentBag是否已被關(guān)閉

    private final SynchronousQueue<T> handoffQueue; //這是個(gè)即產(chǎn)即銷的隊(duì)列,用于在連接不夠用時(shí)胯甩,及時(shí)獲取到add方法里新創(chuàng)建的連接對(duì)象昧廷,詳情可以參考下面borrow和add的代碼

    //內(nèi)部接口,PoolEntry類實(shí)現(xiàn)了該接口
    public interface IConcurrentBagEntry {

        //連接對(duì)象的狀態(tài)偎箫,前面的流程很多地方都已經(jīng)涉及到了木柬,比如主流程4的縮容
        int STATE_NOT_IN_USE = 0; //閑置
        int STATE_IN_USE = 1; //使用中
        int STATE_REMOVED = -1; //已廢棄
        int STATE_RESERVED = -2; //標(biāo)記保留,介于閑置和廢棄之間的中間狀態(tài)淹办,主要由縮容那里觸發(fā)修改

        boolean compareAndSet(int expectState, int newState); //嘗試?yán)胏as修改連接對(duì)象的狀態(tài)值

        void setState(int newState); //設(shè)置狀態(tài)值

        int getState(); //獲取狀態(tài)值
    }

    //參考上面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) {
        //省略...
    }

    //根據(jù)連接狀態(tài)值獲取當(dāng)前池子內(nèi)所有符合條件的連接集合
    public List values(final int state) {
        //省略...
    }

    //獲取當(dāng)前池子內(nèi)所有的連接
    public List values() {
        //省略...
    }

    //利用cas把傳入的連接對(duì)象的state從 STATE_NOT_IN_USE 變?yōu)?STATE_RESERVED
    public boolean reserve(final T bagEntry) {
        //省略...
    }

    //獲取當(dāng)前池子內(nèi)符合傳入狀態(tài)值的連接數(shù)量
    public int getCount(final int state) {
        //省略...
    }
}

從這個(gè)基本結(jié)構(gòu)就可以稍微看出HikariCP是如何優(yōu)化傳統(tǒng)連接池實(shí)現(xiàn)的了眉枕,相比Druid來說,HikariCP更加偏向無鎖實(shí)現(xiàn)娇唯,盡量避免鎖競(jìng)爭(zhēng)的發(fā)生齐遵。

12.1:borrow

這個(gè)方法用來獲取一個(gè)可用的連接對(duì)象,觸發(fā)點(diǎn)為流程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(); //首先從當(dāng)前線程的緩存里拿到之前被緩存進(jìn)來的連接對(duì)象集合
    for (int i = list.size() - 1; i >= 0; i--) {
        final Object entry = list.remove(i); //先移除,回收方法那里會(huì)再次add進(jìn)來
        final T bagEntry = weakThreadLocals ? ((WeakReference<T>) entry).get() : (T) entry; //默認(rèn)不啟用弱引用
        // 獲取到對(duì)象后想许,通過cas嘗試把其狀態(tài)從STATE_NOT_IN_USE 變?yōu)?STATE_IN_USE伶授,注意,這里如果其他線程也在使用這個(gè)連接對(duì)象流纹,
        // 并且成功修改屬性糜烹,那么當(dāng)前線程的cas會(huì)失敗,那么就會(huì)繼續(xù)循環(huán)嘗試獲取下一個(gè)連接對(duì)象
        if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
            return bagEntry; //cas設(shè)置成功后漱凝,表示當(dāng)前線程繞過其他線程干擾疮蹦,成功獲取到該連接對(duì)象,直接返回
        }
    }

    // 源注釋:Otherwise, scan the shared list ... then poll the handoff queue
    final int waiting = waiters.incrementAndGet(); //如果緩存內(nèi)找不到一個(gè)可用的連接對(duì)象茸炒,則認(rèn)為需要“回源”愕乎,waiters+1
    try {
        for (T bagEntry : sharedList) {
            //循環(huán)sharedList阵苇,嘗試把連接狀態(tài)值從STATE_NOT_IN_USE 變?yōu)?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) { //阻塞線程數(shù)大于1時(shí),需要觸發(fā)HikariPool的addBagItem方法來進(jìn)行添加連接入池感论,這個(gè)方法的實(shí)現(xiàn)參考主流程3
                    listener.addBagItem(waiting - 1);
                }
                return bagEntry; //cas設(shè)置成功绅项,跟上面的邏輯一樣,表示當(dāng)前線程繞過其他線程干擾比肄,成功獲取到該連接對(duì)象快耿,直接返回
            }
        }

        //走到這里說明不光線程緩存里的列表競(jìng)爭(zhēng)不到連接對(duì)象,連sharedList里也找不到可用的連接芳绩,這時(shí)則認(rèn)為需要通知HikariPool掀亥,該觸發(fā)添加連接操作了
        listener.addBagItem(waiting);

        timeout = timeUnit.toNanos(timeout); //這時(shí)候開始利用timeout控制獲取時(shí)間
        do {
            final long start = currentTime();
            //嘗試從handoffQueue隊(duì)列里獲取最新被加進(jìn)來的連接對(duì)象(一般新入的連接對(duì)象除了加進(jìn)sharedList之外,還會(huì)被offer進(jìn)該隊(duì)列)
            final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS);
            //如果超出指定時(shí)間后仍然沒有獲取到可用的連接對(duì)象妥色,或者獲取到對(duì)象后通過cas設(shè)置成功铺浇,這兩種情況都不需要重試,直接返回對(duì)象
            if (bagEntry == null || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
                return bagEntry;
            }
            //走到這里說明從隊(duì)列內(nèi)獲取到了連接對(duì)象垛膝,但是cas設(shè)置失敗,說明又該對(duì)象又被其他線程率先拿去用了丁稀,若時(shí)間還夠吼拥,則再次嘗試獲取
            timeout -= elapsedNanos(start); //timeout減去消耗的時(shí)間,表示下次循環(huán)可用時(shí)間
        } while (timeout > 10_000); //剩余時(shí)間大于10s時(shí)才繼續(xù)進(jìn)行线衫,一般情況下凿可,這個(gè)循環(huán)只會(huì)走一次,因?yàn)閠imeout很少會(huì)配的比10s還大

        return null; //超時(shí)授账,仍然返回null
    } finally {
        waiters.decrementAndGet(); //這一步出去后枯跑,HikariPool收到borrow的結(jié)果,算是走出阻塞白热,所以waiters-1
    }
}

仔細(xì)看下注釋敛助,該過程大致分成三個(gè)主要步驟:

  1. 從線程緩存獲取連接
  2. 獲取不到再?gòu)?code>sharedList里獲取
  3. 都獲取不到則觸發(fā)添加連接邏輯,并嘗試從隊(duì)列里獲取新生成的連接對(duì)象

12.2:add

這個(gè)流程會(huì)添加一個(gè)連接對(duì)象進(jìn)入bag屋确,通常由主流程3里的addBagItem方法通過addConnectionExecutor異步任務(wù)觸發(fā)添加操作纳击,該方法主流程如下:

public void add(final T bagEntry) {

    sharedList.add(bagEntry); //直接加到sharedList里去

    // 源注釋:spin until a thread takes it or none are waiting
    // 參考borrow流程,當(dāng)存在線程等待獲取可用連接攻臀,并且當(dāng)前新入的這個(gè)連接狀態(tài)仍然是閑置狀態(tài)焕数,且隊(duì)列里無消費(fèi)者等待獲取時(shí),發(fā)起一次線程調(diào)度
    while (waiters.get() > 0 && bagEntry.getState() == STATE_NOT_IN_USE && !handoffQueue.offer(bagEntry)) { //注意這里會(huì)offer一個(gè)連接對(duì)象入隊(duì)列
        yield();
    }
}

結(jié)合borrow來理解的話刨啸,這里在存在等待線程時(shí)會(huì)添加一個(gè)連接對(duì)象入隊(duì)列堡赔,可以讓borrow里發(fā)生等待的地方更容易poll到這個(gè)連接對(duì)象。

12.3:requite

這個(gè)流程會(huì)回收一個(gè)連接设联,該方法的觸發(fā)點(diǎn)在主流程6善已,具體代碼如下:

public void requite(final T bagEntry) {
    bagEntry.setState(STATE_NOT_IN_USE); //回收意味著使用完畢灼捂,更改state為STATE_NOT_IN_USE狀態(tài)

    for (int i = 0; waiters.get() > 0; i++) { //如果存在等待線程的話,嘗試傳給隊(duì)列雕拼,讓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) { //線程內(nèi)連接集合的緩存最多50個(gè)纵东,這里回收連接時(shí)會(huì)再次加進(jìn)當(dāng)前線程的緩存里,方便下次borrow獲取
        threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry); //默認(rèn)不啟用弱引用啥寇,若啟用的話偎球,則緩存集合里的連接對(duì)象沒有內(nèi)存泄露的風(fēng)險(xiǎn)
    }
}

12.4:remove

這個(gè)負(fù)責(zé)從池子里移除一個(gè)連接對(duì)象,觸發(fā)點(diǎn)在流程1.1.2辑甜,代碼如下:

public boolean remove(final T bagEntry) {
    // 下面兩個(gè)cas操作衰絮,都是從其他狀態(tài)變?yōu)橐瞥隣顟B(tài),任意一個(gè)成功磷醋,都不會(huì)走到下面的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;
}

這里需要注意的是猫牡,移除時(shí)僅僅移除了sharedList里的對(duì)象,各個(gè)線程內(nèi)緩存的那一份集合里對(duì)應(yīng)的對(duì)象并沒有被移除邓线,這個(gè)時(shí)候會(huì)不會(huì)存在該連接再次從緩存里拿到呢淌友?會(huì)的,但是不會(huì)返回出去骇陈,而是直接remove掉了震庭,仔細(xì)看borrow的代碼發(fā)現(xiàn)狀態(tài)不是閑置狀態(tài)的時(shí)候,取出來時(shí)就會(huì)remove掉你雌,然后也拿不出去器联,自然也不會(huì)觸發(fā)回收方法。

12.5:values

該方法存在重載方法婿崭,用于返回當(dāng)前池子內(nèi)連接對(duì)象的集合拨拓,觸發(fā)點(diǎn)在主流程4,代碼如下:

public List values(final int state) {
    //過濾出來符合狀態(tài)值的對(duì)象集合逆序后返回出去
    final List list = sharedList.stream().filter(e -> e.getState() == state).collect(Collectors.toList());
    Collections.reverse(list);
    return list;
}

public List values() {
    //返回全部連接對(duì)象(注意下方clone為淺拷貝)
    return (List) sharedList.clone();
}

12.6:reserve

該方法單純將連接對(duì)象的狀態(tài)值由STATE_NOT_IN_USE修改為STATE_RESERVED氓栈,觸發(fā)點(diǎn)仍然是主流程4渣磷,縮容時(shí)使用,代碼如下:

public boolean reserve(final T bagEntry){
   return bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_RESERVED);
}

12.7:getCount

該方法用于返回池內(nèi)符合某個(gè)狀態(tài)值的連接的總數(shù)量授瘦,觸發(fā)點(diǎn)為主流程5幸海,擴(kuò)充連接池時(shí)用于獲取閑置連接總數(shù),代碼如下:

public int getCount(final int state){
   int count = 0;
   for (IConcurrentBagEntry e : sharedList) {
      if (e.getState() == state) {
         count++;
      }
   }
   return count;
}

以上就是ConcurrentBag的主要方法和處理連接對(duì)象的主要流程奥务。

十三物独、總結(jié)

到這里基本上一個(gè)連接的生產(chǎn)到獲取到回收到廢棄一整個(gè)生命周期在HikariCP內(nèi)是如何管理的就說完了,相比之前的Druid的實(shí)現(xiàn)氯葬,有很大的不同挡篓,主要是HikariCP的無鎖獲取連接,本篇沒有涉及FastList的說明,因?yàn)閺倪B接管理這個(gè)角度確實(shí)很少用到該結(jié)構(gòu)官研,用到FastList的地方主要在存儲(chǔ)連接對(duì)象生成的statement對(duì)象以及用于存儲(chǔ)線程內(nèi)緩存起來的連接對(duì)象秽澳;

除此之外HikariCP還利用javassist技術(shù)編譯期生成了ProxyConnection的初始化,這里也沒有相關(guān)說明戏羽,網(wǎng)上有關(guān)HikariCP的優(yōu)化有很多文章担神,大多數(shù)都提到了字節(jié)碼優(yōu)化fastList始花、concurrentBag的實(shí)現(xiàn)妄讯,本篇主要通過深入解析HikariPoolConcurrentBag的實(shí)現(xiàn),來說明HikariCP相比Druid具體做了哪些不一樣的操作酷宵。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末亥贸,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子浇垦,更是在濱河造成了極大的恐慌炕置,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,123評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件男韧,死亡現(xiàn)場(chǎng)離奇詭異朴摊,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)此虑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門仍劈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人寡壮,你說我怎么就攤上這事《锿洌” “怎么了况既?”我有些...
    開封第一講書人閱讀 156,723評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)组民。 經(jīng)常有香客問我棒仍,道長(zhǎng),這世上最難降的妖魔是什么臭胜? 我笑而不...
    開封第一講書人閱讀 56,357評(píng)論 1 283
  • 正文 為了忘掉前任莫其,我火速辦了婚禮,結(jié)果婚禮上耸三,老公的妹妹穿的比我還像新娘乱陡。我一直安慰自己,他們只是感情好仪壮,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評(píng)論 5 384
  • 文/花漫 我一把揭開白布憨颠。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪爽彤。 梳的紋絲不亂的頭發(fā)上养盗,一...
    開封第一講書人閱讀 49,760評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音适篙,去河邊找鬼往核。 笑死,一個(gè)胖子當(dāng)著我的面吹牛嚷节,可吹牛的內(nèi)容都是我干的聂儒。 我是一名探鬼主播,決...
    沈念sama閱讀 38,904評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼丹喻,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼薄货!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起碍论,我...
    開封第一講書人閱讀 37,672評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤谅猾,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后鳍悠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體税娜,經(jīng)...
    沈念sama閱讀 44,118評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評(píng)論 2 325
  • 正文 我和宋清朗相戀三年藏研,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了敬矩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,599評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蠢挡,死狀恐怖弧岳,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情业踏,我是刑警寧澤禽炬,帶...
    沈念sama閱讀 34,264評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站勤家,受9級(jí)特大地震影響腹尖,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜伐脖,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評(píng)論 3 312
  • 文/蒙蒙 一热幔、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧讼庇,春花似錦绎巨、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春却嗡,著一層夾襖步出監(jiān)牢的瞬間舶沛,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評(píng)論 1 264
  • 我被黑心中介騙來泰國(guó)打工窗价, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留如庭,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,286評(píng)論 2 360
  • 正文 我出身青樓撼港,卻偏偏與公主長(zhǎng)得像坪它,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子帝牡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容