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

相关文章
相关标签/搜索