十一、HikariCP 源码分析之 HouseKeeper
创始人
2025-05-29 11:57:03
0
源代码版本 2.4.5-SNAPSHOT

HouseKeeper 是一个HikariPool的内部类,它实现了Runnable接口,也就是一个线程任务。这个任务是由ScheduledThreadPoolExecutor类型的线程池执行的,也就是说它是一个定时任务。我们在《HikariCP 源码分析之初始化分析二》中分析 HikariCP 初始化的时候,遇到了houseKeepingExecutorService的初始化,简单分析了它的初始化过程,但是这个任务是非常重要的,我们要仔细分析一下。

它的主要作用就是:检测时间回拨,并关闭空闲时间超期的连接。下面的代码依然是有详细的注释,同时也记录了我当时自己分析代码时遇到的一些疑问和解答。

我们看下代码:

/*** HouseKeeper用于空闲连接过期*/
private class HouseKeeper implements Runnable {private volatile long previous = clockSource.plusMillis(clockSource.currentTime(), -HOUSEKEEPING_PERIOD_MS);@Overridepublic void run() {//①//刷新通过MBean修改的设置connectionTimeout = config.getConnectionTimeout();validationTimeout = config.getValidationTimeout();leakTask.updateLeakDetectionThreshold(config.getLeakDetectionThreshold());//②final long idleTimeout = config.getIdleTimeout();final long now = clockSource.currentTime();//检测时间回拨, 即网络对时服务对时钟的调整, 允许 128 毫秒的时间差if (clockSource.plusMillis(now, 128) < clockSource.plusMillis(previous, HOUSEKEEPING_PERIOD_MS)) {LOGGER.warn("{} - Retrograde clock change detected (housekeeper delta={}), soft-evicting connections from pool.",clockSource.elapsedDisplayString(previous, now), poolName);previous = now;//连接池中所以的连接都标记删除softEvictConnections();//重新创建连fillPool();return;} else if (now > clockSource.plusMillis(previous, (3 * HOUSEKEEPING_PERIOD_MS) / 2)) {// No point evicting for forward clock motion, this merely accelerates connection retirement anyway//时钟快了, 没必要调整连接池, 反正是加速了连接的过期, 不影响LOGGER.warn("{} - Thread starvation or clock leap detected (housekeeper delta={}).", clockSource.elapsedDisplayString(previous, now), poolName);}//原来的实现代码如下文件的633-650 行: https://github.com/brettwooldridge/HikariCP/blob/bc010fba486b27ae3d034cc9701e0c4217457ddb/src/main/java/com/zaxxer/hikari/pool/HikariPool.java//         logPoolState("Before cleanup ");//         for (PoolBagEntry bagEntry : connectionBag.values(STATE_NOT_IN_USE)) {//            if (connectionBag.reserve(bagEntry)) {//               if (bagEntry.evicted) {//                  closeConnection(bagEntry, "(connection evicted)");//               }//               else if (idleTimeout > 0L && clockSource.elapsedMillis(bagEntry.lastAccess, now) > idleTimeout) {//                  closeConnection(bagEntry, "(connection passed idleTimeout)");//               }//               else {//                  connectionBag.unreserve(bagEntry);//               }//            }//         }////         logPoolState("After cleanup ");////         fillPool(); // Try to maintain minimum connections//      }// 代码中, 先将所有超过空闲时间的连接都关闭, 然后将连接池中的连接再填充到minIdle最小空闲连接数// 后来有个名为 yaojuncn 的人跟brett提了个 issue, 如下: https://github.com/brettwooldridge/HikariCP/issues/379// issue的内容就是yaojuncn发现, 清理空闲连接的时候, 连接数会小于minIdle, 极端情况下会是 0, 他认为这样有问题, 服务请求多的时候, 会大量的创建连接, 给数据库造成压力// 但是brett认为, 创建连接非常快, 极端情况的几率极小, 这不是个问题, 提议yaojuncn使用固定大小的连接池// 讨论来讨论去, brett终于烦了, 接受了yaojuncn的建议, 并且合并了yaojuncn的 merge request.// yaojuncn提的实现就是目前的请清理方式, 这个: https://github.com/yaojuncn/HikariCP/commit/cbb1e1cc93d050457ffe9939b67eacd6c6bd97a0//③//开始清理超过idleTimeout的空闲连接previous = now;String afterPrefix = "Pool ";if (idleTimeout > 0L) {//查出连接池中所有的空闲连接final List idleList = connectionBag.values(STATE_NOT_IN_USE);//空闲连接数量 - 用户配置的最小连接数 = 目前可以回收的连接数, 不明白详见Question①int removable = idleList.size() - config.getMinimumIdle();//如果有可以回收的连接if (removable > 0) {logPoolState("Before cleanup ");afterPrefix = "After cleanup  ";// 按照最近访问的实际, 从小到大排序, 排序指标是最后访问时间的时间戳, 时间大的是最近使用的, 从小到大遍历比较合理, 能先清理掉长时间没用的, 不用遍历所有的空闲连接//如果要清理的连接数够了,那么就不用继续遍历了,可以减少循环次数          Collections.sort(idleList, LAST_ACCESS_COMPARABLE);for (PoolEntry poolEntry : idleList) {//判断最后访问时间和当前时间的时间差, 是否超过了用户配置的最大空闲时间, 超过了就将连接变为保留状态if (clockSource.elapsedMillis(poolEntry.lastAccessed, now) > idleTimeout && connectionBag.reserve(poolEntry)) {//关闭连接closeConnection(poolEntry, "(connection has passed idleTimeout)");//可回收连接数减 1, 如果可回收连接数等于 0, 就是清理完了if (--removable == 0) {break; // keep min idle cons}}}}}//记录日志logPoolState(afterPrefix);//可能有些连接过期了, 重新填充连接池到用户配置的最小连接数fillPool(); // Try to maintain minimum connections}
}

①刷新配置

如果你看过《HikariCP 源码分析之获取连接流程二》的话可能还记得,我们是可以通过 JMX 的方式来挂起整个连接池的,此时连接池是不可用的状态,然后我们就可以修改连接池的一些配置,然后将连接池恢复。修改了配置之后,并不是立即生效的,因为配置是在这里刷新的,而这里是一个定时任务,是每 30 秒触发一次。

为什么不能立即修改这些配置呢?

因为是在运行期修改的配置,你修改配置之后,连接池中之前的连接还是原来的配置呢,总得要处理一下这些连接吧?而这个处理过程正好是 HouseKeeper 的职责范围,因此就在这里刷新配置了,而且使用 JMX 修改配置这个需求,对时效性实在没有什么要求,30 秒完全可以接受,毕竟这不是一个常规的操作,一般是测试用途。

至于刷新的配置内容,略过,大家可以看下配置分析那一节的内容。

②时间回拨

这里比较有意思,通常我们的服务器都是有网络对时服务的,如果本地的系统时间不对的话就会自动调整。但是我们的 HikariCP 中的定时任务是依赖系统时间的啊,如果时间被调整了,那么定时任务就错乱了,后果非常严重,会导致该回收的连接回收不了。

开始正式任务之前,idleTimeout和now依然是准备工作,idleTimeout是用户的配置项,连接的最大空闲时间,而now就是当前的系统时间。

我们看下这个 if 判断clockSource.plusMillis(now, 128) < clockSource.plusMillis(previous, HOUSEKEEPING_PERIOD_MS),clockSource.plusMillis(now, 128)的意思是当前时间加 128 毫秒,clockSource是一个时间工具类。previous是上一次任务的执行时间,HOUSEKEEPING_PERIOD_MS是任务的执行间隔,它们相加也就是本次任务应该执行的时间。如果当前时间+128 毫秒,小于,当前任务应该触发的时间,那么就是系统时间回退了 128 毫秒以上对吧?这个是不行的。

有两种情况 HikariCP 是可以容忍的:

系统时间回退 128 毫秒以内

系统时间前进了,具体多长时间不管

上面两种情况下,是不会进入 if 条件里的。但是我们还是要分析一下的,假如我们进入了 if 代码块,previous = now;这个的业务意思保存一下本次任务执行的时间,因为下次执行任务要用。softEvictConnections();一句简简单单的代码,就将连接池中所有的连接都驱逐出去了,连接首先会被标记删除,然后就真的关闭了这个连接,我们后面可以出一个 HikariCP 中连接关闭的单独文章分析下。fillPool();含义一眼就能看出来,是重新填充连接池,重新创建连接加入到连接池中。

如果是 else-if ,那么就是系统时间被调快了,这个只是加速了连接的生命结束,对 HikariCP 没有影响,连接被回收了是会自动创建新的连接,这个没有关系,因此不处理,只是打印一个警告。

③清理过期连接

我们直接看 if 里面,使用connectionBag.values(STATE_NOT_IN_USE)方法查询出来所有的空闲状态的连接,int removable = idleList.size() - config.getMinimumIdle();计算了当前空闲连接数超出用户配置数几个,也就是要清理的连接个数。

如果removable大于 0,那么确实有需要清理的连接。

这里Collections.sort(idleList, LAST_ACCESS_COMPARABLE);先对所有的空闲连接按照最后使用时间从小到大进行排序,因为每个连接上都记录了最后使用时间,时间戳越小的,说明它最后使用时间越早,越大的越是最近使用过的。最近使用过的连接很可能被某个线程保存在本地的 ThreadLocal 中了,我们不清理这些连接,方便线程下次使用的时候直接获取。

然后循环遍历排序后的空闲连接,if 的条件是clockSource.elapsedMillis(poolEntry.lastAccessed, now) > idleTimeout && connectionBag.reserve(poolEntry),如果clockSource.elapsedMillis(poolEntry.lastAccessed, now),它是连接上次使用时间距离当前时间的时间差,大于用户配置的连接的最大空闲时间,说明这个连接空闲了太久,需要回收,此时在 if 条件中就执行connectionBag.reserve(poolEntry),修改连接的状态,修改成功之后,closeConnection方法是关闭这个底层连接,是真的被关闭了,然后可回收连接数减 1,继续循环,直到可回收连接数为 0 ,说明已经达到了用户的配置要求,结束。

但是有个问题,我们是回收了多余的空闲连接,假如在这期间,有其他连接生命周期时间到了,被关闭了,或者是连接发生了致命错误,被关闭了。那么,现在剩下的连接数又少于用户配置的最小空闲连接数了,怎么办呢?

这就是fillPool()的作用,它会将连接池中的连接数量重新填充到最小连接数。

好了,至此,HouseKeeper 的功能我们分析完了

相关内容

热门资讯

银行、消金公司助贷余额增速不得... 近日,中国证券报记者从多位业内人士处独家获悉,5月以来,多地金融监管部门对部分中小银行、消金公司下达...
朱鸿接任陈航,担任钉钉科技有限... 消费日报-今朝新闻讯 天眼查显示,6月23日,钉钉科技有限公司发生工商变更,陈航卸任法定代表人、董事...
3日累跌超20%,德创环保:公... 6月25日, 德创环保(603177.SH)公告,公司股票于2026年6月23日、6月24日和6月2...
北京发布2026年第七轮拟供商... 央广网北京6月25日消息(记者门庭婷)6月25日,北京市规划和自然资源委员会网站发布了2026年第七...
开放麦 | 启明创投胡奇:从A... “2026年,创投圈的浪潮再次翻涌:AI从技术概念走进产业深水区,硬科技创业从“小众赛道” 变成“主...
腾讯孙忠怀:在行业转身处 6月24日,2026腾讯视频年度发布在上海举行。腾讯公司副总裁、腾讯在线视频董事长孙忠怀以《在行业转...
加息,突变!美联储,重磅传来!... 美联储政策路径突生变数。 美国商务部经济分析局最新公布的数据显示,5月个人消费支出(PCE)物价指数...
6月合肥上门收金必看!5步避坑... 2026年6月,合肥黄金市场持续高位运行,不少市民翻出家里闲置的旧金饰、投资金条想变现,上门回收因为...
潮汕女富豪挂帅后加码液冷!祥鑫... 潮汕女强人,带着百亿公司加码液冷散热。 6月24日晚间,祥鑫科技(002965.SZ)公告称,公司董...
马斯克向太空要电,GobiX ... 一场关于「去哪里找电」的全球竞赛,正在朝两个方向展开。 作者|周永亮 编辑| 郑玄 「太空光伏是不是...
原料药行业陷入周期低谷 有药企... 每经记者|许立波 每经编辑|魏文艺 “过完年到现在,我们整个团队每个月都在出差,跑遍了亚非拉、欧美市...
家门口筛查白内障!永顺泽家镇暖... 大众卫生报·新湖南客户端6月25日讯(通讯员 彭雪姣)为切实解决辖区老年性白内障患者异地就医奔波、就...
终于等到!油价马上再大跌,这个... 点击添加图片描述(最多60个字) 编辑 各位车主朋友,好消息接二连三! 继6月18日油价大幅下调...
丈量出海新路 世界酒庄影响力指... 长期以来,全球酒庄评价体系由西方机构主导,且大多局限于单一酒种、单一评价维度,这一局面正逐渐被打破。...
峰瑞资本创始合伙人李丰:从资本... “2026年,创投圈的浪潮再次翻涌:AI从技术概念走进产业深水区,硬科技创业从“小众赛道” 变成“主...
原创 A... 迈向成熟,还有茁壮成长的机会。 作者 | 方璐 编辑丨于婞 来源 | 野马财经 2026年6月21日...
为企业解锁出海新通道!亚太中小... 6月24日下午,作为2026年APEC中小企业工商论坛的重要组成部分,亚太中小企业国际化合作发展论坛...
君赛生物港股IPO,增聘兴证国... 跟丰宜科技一样,正冲刺港股IPO的上海君赛生物股份有限公司(简称“君赛生物”)增聘一位整体协调人。 ...
圣邦股份明日上市:暗盘涨24%... 雷递网 雷建平 6月25日 圣邦微电子(北京)股份有限公司(简称:“圣邦股份”,股票代码:“0366...
科技“吃肉”,券商跟着“喝汤”... 当科技持续成为市场核心主线,押中硬科技项目的券商也成为被追逐的焦点。 6月24日,半导体零部件概念股...