Spring Boot 青睐的数据库链接池HikariCP为何是史上最快的?

前言

如今已经有不少公司在使用HikariCP了,HikariCP还成为了SpringBoot默认的链接池,伴随着SpringBoot和微服务,HikariCP 必将迎来普遍的普及。java

下面陈某带你们从源码角度分析一下HikariCP为何可以被Spring Boot 青睐,文章目录以下:mysql

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=目录sql

 

零、类图和流程图

开始前先来了解下HikariCP获取一个链接时类间的交互流程,方便下面详细流程的阅读。缓存

获取链接时的类间交互:网络

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=图1多线程

 

1、主流程1:获取链接流程

HikariCP获取链接时的入口是HikariDataSource里的getConnection方法,如今来看下该方法的具体流程:并发

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=主流程1框架

上述为HikariCP获取链接时的流程图,由图1可知,每一个datasource对象里都会持有一个HikariPool对象,记为pool,初始化后的datasource对象pool是空的,因此第一次getConnection的时候会进行实例化pool属性(参考主流程1),初始化的时候须要将当前datasource里的config属性传过去,用于pool的初始化,最终标记sealed,而后根据pool对象调用getConnection方法(参考流程1.1),获取成功后返回链接对象。dom

 

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

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=主流程2异步

该流程用于初始化整个链接池,这个流程会给链接池内全部的属性作初始化的工做,其中比较主要的几个流程上图已经指出,简单归纳一下:

  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_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=流程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线程池,异步关闭驱动链接)。

 

4、流程1.1.1:链接判活

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=流程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属性。

 

5、流程1.1.2:关闭链接对象

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=流程1.1.2

这个流程简单来讲就是把流程1.1.1中验证不经过的死链接,主动关闭的一个流程,首先会把这个链接对象从ConnectionBag里移除,而后把实际的物理链接交给一个线程池去异步执行,这个线程池就是在主流程2里初始化池的时候初始化的线程池closeConnectionExecutor,而后异步任务内开始实际的关链接操做,由于主动关闭了一个链接至关于少了一个链接,因此还会触发一次扩充链接池(参考主流程5)操做。

 

6、流程2.1:HikariCP监控设置

不一样于Druid那样监控指标那么多,HikariCP会把咱们很是关心的几项指标暴露给咱们,好比当前链接池内闲置链接数、总链接数、一个链接被用了多久归还、建立一个物理链接花费多久等,HikariCP的链接池的监控咱们这一节专门详细的分解一下,首先找到HikariCP下面的metrics文件夹,这下面放置了一些规范实现的监控接口等,还有一些现成的实现(好比HikariCP自带对prometheus、micrometer、dropwizard的支持,不太了解后面两个,prometheus下文直接称为普罗米修斯):

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=图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的监控器就算是注册进去了,因此要想实现本身的监控器拿到上面的指标,要通过以下步骤:

  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原生的普罗米修斯收集器里面对监控指标的命名并不符合我司的规范,因此就自定义了一个,有相似问题的不妨也试一试。

???? 这一节没有画图,纯代码,由于画图不太好解释这部分的东西,这部份内容与链接池总体流程关系也不大,充其量获取了链接池自己的一些属性,在链接池里的触发点也在上面代码段的注释里说清楚了,看代码定义可能更好理解一些。

 

7、流程2.2:链接泄漏的检测与告警

本节对应主流程2里的子流程2.2,在初始化池对象时,初始化了一个叫作leakTaskFactory的属性,本节来看下它具体是用来作什么的。

7.1:它是作什么的?

一个链接被拿出去使用时间超过leakDetectionThreshold(可配置,默认0)未归还的,会触发一个链接泄漏警告,通知业务方目前存在链接泄漏的问题。

7.2:过程详解

该属性是ProxyLeakTaskFactory类型对象,且它还会持有houseKeepingExecutorService这个线程池对象,用于生产ProxyLeakTask对象,而后利用上面的houseKeepingExecutorService延时运行该对象里的run方法。该流程的触发点在上面的流程1.1最后包装成ProxyConnection对象的那一步,来看看具体的流程图:

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=流程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的大小也是须要考虑的一件事。

 

8、主流程3:生成链接对象

本节来说下主流程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线程池执行,那么,如今来看下这个异步添加链接对象的任务流程:

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=addConnectionExecutor的call流程

这个流程就是往链接池里加链接用的,跟createEntry结合起来讲是由于这俩流程是紧密相关的,除此以外,主流程5(fillPool,扩充链接池)也会触发该任务。

 

9、主流程4:链接池缩容

HikariCP会按照minIdle定时清理闲置太久的链接,这个定时任务在主流程2初始化链接池对象时被启用,跟上面的流程同样,也是利用houseKeepingExecutorService这个线程池对象作该定时任务的执行器。

来看下主流程2里是怎么启用该任务的:

//housekeepingPeriodMs的默认值是30s,因此定时任务的间隔为30s
this.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, housekeepingPeriodMs, MILLISECONDS);

那么本节主要来讲下HouseKeeper这个类,该类实现了Runnable接口,回收逻辑主要在其run方法内,来看看run方法的逻辑流程图:

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=主流程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的时候才会启用,默认是不启用的,若须要启用,能够按照条件来配置。

 

10、主流程5:扩充链接池

这个流程主要依附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配置的值同样的话,在池内链接吃紧的时候,就不会发生任何扩容了。

 

11、主流程6:链接回收

最开始说过,最终真实的物理链接对象会被包装成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方法部分
}

以上就是链接回收部分的逻辑,相比其余流程,仍是比较简洁的。

 

12、ConcurrentBag主流程

这个类用来存放最终的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更加偏向无锁实现,尽可能避免锁竞争的发生。

12.1:borrow

这个方法用来获取一个可用的链接对象,触发点为流程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
    }
}

仔细看下注释,该过程大体分红三个主要步骤:

  1. 从线程缓存获取链接

  2. 获取不到再从sharedList里获取

  3. 都获取不到则触发添加链接逻辑,并尝试从队列里获取新生成的链接对象

12.2:add

这个流程会添加一个链接对象进入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到这个链接对象。

12.3:requite

这个流程会回收一个链接,该方法的触发点在主流程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); //默认不启用弱引用,若启用的话,则缓存集合里的链接对象没有内存泄露的风险
    }
}

12.4:remove

这个负责从池子里移除一个链接对象,触发点在流程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掉,而后也拿不出去,天然也不会触发回收方法。

12.5:values

该方法存在重载方法,用于返回当前池子内链接对象的集合,触发点在主流程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();
}

12.6:reserve

该方法单纯将链接对象的状态值由STATE_NOT_IN_USE修改成STATE_RESERVED,触发点仍然是主流程4,缩容时使用,代码以下:

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

12.7:getCount

该方法用于返回池内符合某个状态值的链接的总数量,触发点为主流程5,扩充链接池时用于获取闲置链接总数,代码以下:

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

以上就是ConcurrentBag的主要方法和处理链接对象的主要流程。

 

十3、总结

到这里基本上一个链接的生产到获取到回收到废弃一整个生命周期在HikariCP内是如何管理的就说完了,相比以前的Druid的实现,有很大的不一样,主要是HikariCP的无锁获取链接,本篇没有涉及FastList的说明,由于从链接管理这个角度确实不多用到该结构,用到FastList的地方主要在存储链接对象生成的statement对象以及用于存储线程内缓存起来的链接对象;

除此以外HikariCP还利用javassist技术编译期生成了ProxyConnection的初始化,这里也没有相关说明,网上有关HikariCP的优化有不少文章,大多数都提到了字节码优化、fastList、concurrentBag的实现,本篇主要经过深刻解析HikariPool和ConcurrentBag的实现,来讲明HikariCP相比Druid具体作了哪些不同的操做。

 

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

相关文章
相关标签/搜索