终于理解 Spring Boot为什么如此受青睐 HikariCP了,这图太透彻

前言

现在已经有很多公司在使用HikariCP了,HikariCP还成为了SpringBoot默认的连接池,伴随着SpringBoot和微服务,HikariCP 必将迎来广泛的普及。

下面陈某带大家从源码角度分析一下HikariCP为什么能够被Spring Boot 请来,文章目录如下:

目录

零、类图和流程图

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

获取连接时的类间交互:

图1

一、主流程1:获取连接流程

HikariCP获取连接时的入口是HikariDataSource里的getConnection方法,现在来看下该方法的具体流程:

主流程1

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

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

主流程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

三、流程1.1:通过HikariPool获取连接对象

流程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.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.2

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

六、流程2.1:HikariCP监控设置

不同于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类,我们来

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值