前言
现在已经有很多公司在使用HikariCP了,HikariCP还成为了SpringBoot默认的连接池,伴随着SpringBoot和微服务,HikariCP 必将迎来广泛的普及。
下面陈某带大家从源码角度分析一下HikariCP为什么能够被Spring Boot 青睐,文章目录如下:
零、类图和流程图
开始前先来了解下HikariCP获取一个连接时类间的交互流程,方便下面详细流程的阅读。
获取连接时的类间交互:
一、主流程1:获取连接流程
HikariCP获取连接时的入口是HikariDataSource
里的getConnection
方法,现在来看下该方法的具体流程:
上述为HikariCP获取连接时的流程图,由图1可知,每个datasource
对象里都会持有一个HikariPool
对象,记为pool,初始化后的datasource对象pool是空的,所以第一次getConnection
的时候会进行实例化pool
属性(参考主流程1
),初始化的时候需要将当前datasource里的config属性
传过去,用于pool的初始化,最终标记sealed
,然后根据pool对象调用getConnection
方法(参考流程1.1
),获取成功后返回连接对象。
二、主流程2:初始化池对象
该流程用于初始化整个连接池
,这个流程会给连接池内所有的属性做初始化的工作,其中比较主要的几个流程上图已经指出,简单概括一下:
- 利用
config
初始化各种连接池属性,并且产生一个用于生产物理连接
的数据源DriverDataSource
- 初始化存放连接对象的核心类
connectionBag
- 初始化一个延时任务线程池类型的对象
houseKeepingExecutorService
,用于后续执行一些延时/定时类任务(比如连接泄漏检查延时任务,参考流程2.2
以及主流程4
,除此之外maxLifeTime
后主动回收关闭连接也是交由该对象来执行的,这个过程可以参考主流程3
) - 预热连接池,HikariCP会在该流程的
checkFailFast
里初始化好一个连接对象放进池子内,当然触发该流程得保证initializationTimeout > 0
时(默认值1),这个配置属性表示留给预热操作的时间(默认值1在预热失败时不会发生重试)。与Druid
通过initialSize
控制预热连接对象数不一样的是,HikariCP仅预热进池一个连接对象。 - 初始化一个线程池对象
addConnectionExecutor
,用于后续扩充连接对象 - 初始化一个线程池对象
closeConnectionExecutor
,用于关闭一些连接对象,怎么触发关闭任务呢?可以参考流程1.1.2
三、流程1.1:通过HikariPool获取连接对象
从最开始的结构图可知,每个HikariPool
里都维护一个ConcurrentBag
对象,用于存放连接对象,由上图可以看到,实际上HikariPool
的getConnection
就是从ConcurrentBag
里获取连接的(调用其borrow
方法获得,对应ConnectionBag主流程
),在长连接检查这块,与之前说的Druid
不同,这里的长连接判活检查在连接对象没有被标记为“已丢弃
”时,只要距离上次使用超过500ms
每次取出都会进行检查(500ms是默认值,可通过配置com.zaxxer.hikari.aliveBypassWindowMs
的系统参数来控制),emmmm,也就是说HikariCP
对长连接的活性检查很频繁,但是其并发性能依旧优于Druid
,说明频繁的长连接检查并不是导致连接池性能高低的关键所在。
作者的Spring Boot 专栏、Mybatis专栏已经完成,关注公众号【码猿技术专栏】回复关键词
Spring Boot 进阶
、Mybatis 进阶
获取。
这个其实是由于HikariCP的无锁
实现,在高并发时对CPU的负载没有其他连接池那么高而产生的并发性能差异,后面会说HikariCP的具体做法,即使是Druid
,在获取连接
、生成连接
、归还连接
时都进行了锁控制
,因为通过上篇解析Druid
的文章可以知道,Druid
里的连接池资源是多线程共享的,不可避免的会有锁竞争,有锁竞争意味着线程状态的变化会很频繁,线程状态变化频繁意味着CPU上下文切换也将会很频繁。
回到流程1.1
,如果拿到的连接为空,直接报错,不为空则进行相应的检查,如果检查通过,则包装成ConnectionProxy
对象返回给业务方,不通过则调用closeConnection
方法关闭连接(对应流程1.1.2
,该流程会触发ConcurrentBag
的remove
方法丢弃该连接,然后把实际的驱动连接交给closeConnectionExecutor
线程池,异步关闭驱动连接)。
四、流程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.1
中验证不通过的死连接,主动关闭的一个流程,首先会把这个连接对象从ConnectionBag
里移除
,然后把实际的物理连接交给一个线程池去异步执行,这个线程池就是在主流程2
里初始化池的时候初始化的线程池closeConnectionExecutor
,然后异步任务内开始实际的关连接操作,因为主动关闭了一个连接相当于少了一个连接,所以还会触发一次扩充连接池(参考主流程5
)操作。
六、流程2.1:HikariCP监控设置
不同于Druid那样监控指标那么多,HikariCP会把我们非常关心的几项指标暴露给我们,比如当前连接池内闲置连接数、总连接数、一个连接被用了多久归还、创建一个物理连接花费多久等,HikariCP的连接池的监控我们这一节专门详细的分解一下,首先找到HikariCP下面的metrics
文件夹,这下面放置了一些规范实现的监控接口等,还有一些现成的实现(比如HikariCP自带对prometheus
、micrometer
、dropwizard
的支持,不太了解后面两个,prometheus
下文直接称为普罗米修斯
):
下面,来着重看下接口的定义:
//这个接口的实现主要负责收集一些动作的耗时
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() {}
}
复制代码
触发点都了解清楚后&#