现在已经有很多公司在使用HikariCP了,HikariCP还成为了SpringBoot默认的连接池,伴随着SpringBoot和微服务,HikariCP 必将迎来广泛的普及。
下面博主带大家从源码角度分析一下HikariCP为什么能够被Spring Boot 青睐,文章目录如下:
开始前先来了解下HikariCP获取一个连接时类间的交互流程,方便下面详细流程的阅读。
获取连接时的类间交互:
HikariCP获取连接时的入口是
HikariDataSource里的
getConnection方法,现在来看下该方法的具体流程:
上述为HikariCP获取连接时的流程图,由图1可知,每个
datasource对象里都会持有一个
HikariPool对象,记为pool,初始化后的datasource对象pool是空的,所以第一次
getConnection的时候会进行
实例化pool属性(参考
主流程1),初始化的时候需要将当前datasource里的
config属性传过去,用于pool的初始化,最终标记
sealed,然后根据pool对象调用
getConnection方法(参考
流程1.1),获取成功后返回连接对象。
该流程用于
初始化整个连接池,这个流程会给连接池内所有的属性做初始化的工作,其中比较主要的几个流程上图已经指出,简单概括一下:
config初始化各种连接池属性,并且产生一个用于
生产物理连接的数据源
DriverDataSourceconnectionBaghouseKeepingExecutorService,用于后续执行一些延时/定时类任务(比如连接泄漏检查延时任务,参考
流程2.2以及
主流程4,除此之外
maxLifeTime后主动回收关闭连接也是交由该对象来执行的,这个过程可以参考
主流程3)checkFailFast里初始化好一个连接对象放进池子内,当然触发该流程得保证
initializationTimeout > 0时(默认值1),这个配置属性表示留给预热操作的时间(默认值1在预热失败时不会发生重试)。与
Druid通过
initialSize控制预热连接对象数不一样的是,HikariCP仅预热进池一个连接对象。addConnectionExecutor,用于后续扩充连接对象closeConnectionExecutor,用于关闭一些连接对象,怎么触发关闭任务呢?可以参考
流程1.1.2
从最开始的结构图可知,每个
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里的判活流程,来看下判活是如何做的,首先说验证方法(注意这里该方法接受的这个
connection对象不是
poolEntry,而是
poolEntry持有的实际驱动的连接对象),在之前介绍Druid的时候就知道,Druid是根据驱动程序里是否存在
ping方法来判断是否启用ping的方式判断连接是否存活,但是到了HikariCP则更加简单粗暴,仅根据是否配置了
connectionTestQuery觉定是否启用ping:
所以一般驱动如果不是特别低的版本,
不建议配置该项,否则便会走
createStatement+excute的方式,相比
ping简单发送心跳数据,这种方式显然更低效。
此外,这里在刚进来还会通过驱动的连接对象重新给它设置一遍
networkTimeout的值,使之变成
validationTimeout,表示一次验证的超时时间,为啥这里要重新设置这个属性呢?因为在使用ping方法校验时,是没办法通过类似
statement那样可以
setQueryTimeout的,所以只能由网络通信的超时时间来控制,这个时间可以通过
jdbc的连接参数
socketTimeout来控制:
这个值最终会被赋值给HikariCP的
networkTimeout字段,这就是为什么最后那一步使用这个字段来还原驱动连接超时属性的原因;说到这里,最后那里为啥要再次还原呢?这就很容易理解了,因为验证结束了,连接对象还存活的情况下,它的
networkTimeout的值这时仍然等于
validationTimeout(不合预期),显然在拿出去用之前,需要恢复成本来的值,也就是HikariCP里的
networkTimeout属性。
这个流程简单来说就是把
流程1.1.1中验证不通过的死连接,主动关闭的一个流程,首先会把这个连接对象从
ConnectionBag里
移除,然后把实际的物理连接交给一个线程池去异步执行,这个线程池就是在
主流程2里初始化池的时候初始化的线程池
closeConnectionExecutor,然后异步任务内开始实际的关连接操作,因为主动关闭了一个连接相当于少了一个连接,所以还会触发一次扩充连接池(参考
主流程5)操作。
不同于Druid那样监控指标那么多,HikariCP会把我们非常关心的几项指标暴露给我们,比如当前连接池内闲置连接数、总连接数、一个连接被用了多久归还、创建一个物理连接花费多久等,HikariCP的连接池的监控我们这一节专门详细的分解一下,首先找到HikariCP下面的
metrics文件夹,这下面放置了一些规范实现的监控接口等,还有一些现成的实现(比如HikariCP自带对
prometheus、
micrometer、
dropwizard的支持,不太了解后面两个,
prometheus下文直接称为
普罗米修斯):
下面,来着重看下接口的定义:
触发点都了解清楚后,再来看看
MetricsTrackerFactory的接口定义:
上面的接口用法见注释,针对新出现的
PoolStats类,我们来看看它做了什么:
实际上这里就是这些属性获取和触发刷新的地方,那么这个对象是在哪里被生成并且丢给
MetricsTrackerFactory的
create方法的呢?这就是本节所需要讲述的要点:
主流程2里的设置监控器的流程,来看看那里发生了什么事吧:
到这里HikariCP的监控器就算是注册进去了,所以要想实现自己的监控器拿到上面的指标,要经过如下步骤:
IMetricsTracker接口,我们这里将该类记为
IMetricsTrackerImplMetricsTrackerFactory接口,我们这里将该类记为
MetricsTrackerFactoryImpl,并且将上面的
IMetricsTrackerImpl在其
create方法内实例化MetricsTrackerFactoryImpl实例化后调用HikariPool的
setMetricsTrackerFactory方法注册到Hikari连接池。上面没有提到
PoolStats里的属性怎么监控,这里来说下,由于
create方法是调用一次就没了,
create方法只是接收了
PoolStats对象的实例,如果不处理,那么随着create调用的结束,这个实例针对监控模块来说就失去持有了,所以这里如果想要拿到
PoolStats里的属性,就需要开启一个
守护线程,让其持有
PoolStats对象实例,并且定时获取其内部属性值,然后
push给监控系统,如果是普罗米修斯等使用
pull方式获取监控数据的监控系统,可以效仿HikariCP原生普罗米修斯监控的实现,自定义一个
Collector对象来接收
PoolStats实例,这样普罗米修斯就可以定期拉取了,比如HikariCP根据普罗米修斯监控系统自己定义的
MetricsTrackerFactory实现(对应
图2里的
PrometheusMetricsTrackerFactory类):
通过上面的解释可以知道在HikariCP中如何自定义一个自己的监控器,以及相比Druid的监控,有什么区别。 工作中很多时候都是需要自定义的,我司虽然也是用的普罗米修斯监控,但是因为HikariCP原生的普罗米修斯收集器里面对监控指标的命名并
不符合我司的规范,所以就
自定义了一个,有类似问题的不妨也试一试。
🍁 这一节没有画图,纯代码,因为画图不太好解释这部分的东西,这部分内容与连接池整体流程关系也不大,充其量获取了连接池本身的一些属性,在连接池里的触发点也在上面代码段的注释里说清楚了,看代码定义可能更好理解一些。
本节对应
主流程2里的
子流程2.2,在初始化池对象时,初始化了一个叫做
leakTaskFactory的属性,本节来看下它具体是用来做什么的。
一个连接被拿出去使用时间超过
leakDetectionThreshold(可配置,默认0)未归还的,会触发一个连接泄漏警告,通知业务方目前存在连接泄漏的问题。
该属性是
ProxyLeakTaskFactory类型对象,且它还会持有
houseKeepingExecutorService这个线程池对象,用于生产
ProxyLeakTask对象,然后利用上面的
houseKeepingExecutorService延时运行该对象里的
run方法。该流程的触发点在上面的
流程1.1最后包装成
ProxyConnection对象的那一步,来看看具体的流程图:
每次在
流程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的大小也是需要考虑的一件事。作者的Spring Boot 专栏、Mybatis专栏已经完成,关注公众号【码猿技术专栏】回复关键词
Spring Boot 进阶、
Mybatis 进阶获取。
本节来讲下
主流程2里的
createEntry方法,这个方法利用PoolBase里的
DriverDataSource对象生成一个实际的连接对象(如果忘记
DriverDatasource是哪里初始化的了,可以看下
主流程2里
PoolBase的
initializeDataSource方法的作用),然后用
PoolEntry类包装成
PoolEntry对象,现在来看下这个包装类有哪些主要属性:
上面就是整个
PoolEntry对象里所有的属性,这里再说下
endOfLife对象,它是一个利用
houseKeepingExecutorService这个线程池对象做的延时任务,这个延时任务一般在创建好连接对象后
maxLifeTime左右的时间触发,具体来看下
createEntry代码:
通过上面的流程,可以知道,HikariCP一般通过
createEntry方法来新增一个连接入池,每个连接被包装成PoolEntry对象,在创建好对象时,同时会提交一个延时任务来关闭废弃该连接,这个时间就是我们配置的
maxLifeTime,为了保证不在同一时间失效,HikariCP还会利用
maxLifeTime减去一个随机数作为最终的延时任务延迟时间,然后在触发废弃任务时,还会触发
addBagItem,进行连接添加任务(因为废弃了一个连接,需要往池子里补充一个),该任务则交给由
主流程2里定义好的
addConnectionExecutor线程池执行,那么,现在来看下这个异步添加连接对象的任务流程:
这个流程就是往连接池里加连接用的,跟
createEntry结合起来说是因为这俩流程是紧密相关的,除此之外,
主流程5(
fillPool,扩充连接池)也会触发该任务。
HikariCP会按照
minIdle定时清理闲置过久的连接,这个定时任务在
主流程2初始化连接池对象时被启用,跟上面的流程一样,也是利用
houseKeepingExecutorService这个线程池对象做该定时任务的执行器。
来看下
主流程2里是怎么启用该任务的:
那么本节主要来说下
HouseKeeper这个类,该类实现了
Runnable接口,回收逻辑主要在其
run方法内,来看看
run方法的逻辑流程图:
上面的流程就是
HouseKeeper的run方法里具体做的事情,由于系统时间回拨会导致该定时任务回收一些连接时产生误差,因此存在如下判断:
这是hikariCP在解决系统时钟被回拨时做出的一种措施,通过流程图可以看到,它是直接把池子里所有的连接对象取出来挨个儿的标记成废弃,并且尝试把状态值修改为
STATE_RESERVED(后面会说明这些状态,这里先不深究)。如果系统时钟没有发生改变(绝大多数情况会命中这一块的逻辑),由图知,会把当前池内所有处于闲置状态(
STATE_NOT_IN_USE)的连接拿出来,然后计算需要检查的范围,然后循环着修改连接的状态:
上面的代码就是流程图里对应的没有回拨系统时间时的流程逻辑。该流程在
idleTimeout大于0(默认等于0)并且
minIdle小于
maxPoolSize的时候才会启用,默认是不启用的,若需要启用,可以按照条件来配置。
这个流程主要依附HikariPool里的
fillPool方法,这个方法已经在上面很多流程里出现过了,它的作用就是在触发连接废弃、连接池连接不够用时,发起扩充连接数的操作,这是个很简单的过程,下面看下源码(为了使代码结构更加清晰,对源码做了细微改动):
由该过程可以知道,最终这个新增连接的任务也是交由
addConnectionExecutor线程池来处理的,而任务的主题也是
PoolEntryCreator,这个流程可以参考
主流程3.
然后
needAdd的推算:
根据这个方式判断,可以保证池内的连接数永远不会超过
maxPoolSize,也永远不会低于
minIdle。在连接吃紧的时候,可以保证每次触发都以
minIdle的数量扩容。因此如果在
maxPoolSize跟
minIdle配置的值一样的话,在池内连接吃紧的时候,就不会发生任何扩容了。
最开始说过,最终真实的物理连接对象会被包装成
PoolEntry对象,存放进
ConcurrentBag,然后获取时,PoolEntry对象又会被再次包装成
ProxyConnection对象暴露给使用方的,那么触发连接回收,实际上就是触发ProxyConnection里的close方法:
这个就是ProxyConnection里的close方法,可以看到它最终会调用PoolEntry的recycle方法进行回收,除此之外,连接对象的最后一次使用时间也是在这个时候刷新的,该时间是个很重要的属性,可以用来判断一个连接对象的闲置时间,来看下PoolEntry的
recycle方法:
之前有说过,每个PoolEntry对象都持有HikariPool的对象,方便触发连接池的一些操作,由上述代码可以看到,最终还是会触发HikariPool里的recycle方法,再来看下HikariPool的recycle方法:
以上就是连接回收部分的逻辑,相比其他流程,还是比较简洁的。
这个类用来存放最终的PoolEntry类型的连接对象,提供了基本的增删查的功能,被HikariPool持有,上面那么多的操作,几乎都是在HikariPool中完成的,HikariPool用来管理实际的连接生产动作和回收动作,实际操作的却是ConcurrentBag类,梳理下上面所有流程的触发点:
ConcurrentBag(构造方法),预热时通过
createEntry拿到连接对象,调用
ConcurrentBag.add添加连接到ConcurrentBag。ConcurrentBag.borrow拿到一个连接对象。ConcurrentBag.requite归还一个连接。ConcurrentBag.remove移除连接对象,由前面的流程可知关闭连接触发点为:连接超过最大生命周期maxLifeTime主动废弃、健康检查不通过主动废弃、连接池缩容。ConcurrentBag.add添加连接到ConcurrentBag,由前面的流程可知添加连接触发点为:连接超过最大生命周期maxLifeTime主动废弃连接后、连接池扩容。ConcurrentBag.values筛选出需要的做操作的连接对象,然后再通过
ConcurrentBag.reserve完成对连接对象状态的修改,然后会通过
流程1.1.2触发关闭和移除连接操作。通过触发点整理,可以知道该结构里的主要方法,就是上面触发点里标记为
标签色的部分,然后来具体看下该类的基本定义和主要方法:
从这个基本结构就可以稍微看出HikariCP是如何优化传统连接池实现的了,相比Druid来说,HikariCP更加偏向无锁实现,尽量避免锁竞争的发生。
这个方法用来获取一个可用的连接对象,触发点为
流程1.1,HikariPool就是利用该方法获取连接的,下面来看下该方法做了什么:
仔细看下注释,该过程大致分成三个主要步骤:
sharedList里获取这个流程会添加一个连接对象进入bag,通常由
主流程3里的
addBagItem方法通过
addConnectionExecutor异步任务触发添加操作,该方法主流程如下:
结合
borrow来理解的话,这里在存在等待线程时会添加一个连接对象入队列,可以让
borrow里发生等待的地方更容易poll到这个连接对象。
这个流程会回收一个连接,该方法的触发点在
主流程6,具体代码如下:
这个负责从池子里移除一个连接对象,触发点在
流程1.1.2,代码如下:
这里需要注意的是,移除时仅仅移除了
sharedList里的对象,各个线程内缓存的那一份集合里对应的对象并没有被移除,这个时候会不会存在该连接再次从缓存里拿到呢?会的,但是不会返回出去,而是直接
remove掉了,仔细看
borrow的代码发现状态不是闲置状态的时候,取出来时就会
remove掉,然后也拿不出去,自然也不会触发回收方法。
该方法存在重载方法,用于返回当前池子内连接对象的集合,触发点在
主流程4,代码如下:
该方法单纯将连接对象的状态值由
STATE_NOT_IN_USE修改为
STATE_RESERVED,触发点仍然是
主流程4,缩容时使用,代码如下:
该方法用于返回池内符合某个状态值的连接的总数量,触发点为
主流程5,扩充连接池时用于获取闲置连接总数,代码如下:
以上就是
ConcurrentBag的主要方法和处理连接对象的主要流程。
到这里基本上一个连接的生产到获取到回收到废弃一整个生命周期在HikariCP内是如何管理的就说完了,相比之前的Druid的实现,有很大的不同,主要是HikariCP的
无锁获取连接,本篇没有涉及
FastList的说明,因为从连接管理这个角度确实很少用到该结构,用到
FastList的地方主要在存储连接对象生成的
statement对象以及用于存储线程内缓存起来的连接对象;作者的Spring Boot 专栏、Mybatis专栏已经完成,关注公众号【码猿技术专栏】回复关键词
Spring Boot 进阶、
Mybatis 进阶获取。
除此之外HikariCP还利用
javassist技术编译期生成了
ProxyConnection的初始化,这里也没有相关说明,网上有关HikariCP的优化有很多文章,大多数都提到了
字节码优化、
fastList、
concurrentBag的实现,本篇主要通过深入解析
HikariPool和
ConcurrentBag的实现,来说明HikariCP相比Druid具体做了哪些不一样的操作。
最近我整理了整套 《JAVA核心知识点总结》,说实话 ,作为一名Java程序员,不论你需不需要面试都应该好好看下这份资料。拿到手总是不亏的~我的不少粉丝也因此拿到腾讯字节快手等公司的Offer
进 【 Java进阶之路群】,找管理员获取哦-!
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70000428/viewspace-2782257/ ,如需转载,请注明出处,否则将追究法律责任。