三、HikariCP获取连接流程源码分析三

2022-06-25 17:50:44 浏览数 (2)

欢迎访问我的博客,同步更新: 枫山别院

源代码版本2.4.5-SNAPSHOT

话接上篇,我们继续分析HikariCP获取连接的过程。

③拿到一个连接

代码语言:java复制
//③
//获取连接的时候, 判断连接是否已经被标记移除
if (poolEntry.isMarkedEvicted() || (clockSource.elapsedMillis(poolEntry.lastAccessed, now) > ALIVE_BYPASS_WINDOW_MS && !isConnectionAlive(poolEntry.connection))) {
    //如果连接超出maxLifetime, 或者连接测试不通过, 就关闭连接
    closeConnection(poolEntry, "(connection is evicted or dead)"); // Throw away the dead connection (passed max age or failed alive test)
    //剩余超时时间
    timeout = hardTimeout - clockSource.elapsedMillis(startTime);
} 

如果connectionBag给我们返回了一个连接,那么需要判断两个条件:

  1. 该连接是否被软驱逐了,poolEntry.isMarkedEvicted()
  2. 该连接是否已经不可用了或者说已经不能通过连接检查,!isConnectionAlive(poolEntry.connection)

为什么需要判断呢?连接池里的连接不应该都是可用的状态吗?

这里涉及到 HikariCP 的一个设计点,HikariCP的连接不是实时从连接池里剔除的,只是给连接上打个标记而已,都是在获取连接的时候检查是否可用,如果不可用的时候才直接从连接池里删除。如果在 HikariCP的任何地方都可能剔除连接,那么剔除连接的地方会比较多,会很乱,也容易引发 bug。反之,把剔除链接的操作收缩到某几个固定的逻辑中,就比较好管理。

  • 软驱逐

我们在上面提到一个软驱逐的地方, 就是挂起连接池修改配置的时候,修改完之后要软驱逐所以的连接,使新配置生效。

其实软驱逐是一个标记状态,是一个软删除,在PoolEntry上,有个状态叫做evict,如果是 true,那么,该连接已经被标记删除,不能使用了。然后某个线程在获取连接的时候,正好拿到了这个连接,判断出来它已经被软驱逐,就触发从连接池删除该连接的逻辑。

关闭连接的逻辑我们后面单独分析,此处就不深入了。

  • 连接可用检查

检查连接是否可用的条件,其实是两个:(clockSource.elapsedMillis(poolEntry.lastAccessed, now) > ALIVE_BYPASS_WINDOW_MS && !isConnectionAlive(poolEntry.connection) 。它们使用 and 连接,也就是这两个条件都必须成立。isConnectionAlive方法比较好理解,我们从字面也能看出这个方法的作用,是判断连接是否还活着。那么前面的条件是什么呢?

我看其他的解析文章根本没有提到这里,我们是要解释一下的。

clockSource.elapsedMillis(poolEntry.lastAccessed, now)这句代码里,poolEntry.lastAccessed是获取连接上次使用的时间,now是当前时间,那么elapsedMillis其实就是计算连接到现在多长时间没有被使用过了,结果是个毫秒数。

ALIVE_BYPASS_WINDOW_MS的定义是private final long ALIVE_BYPASS_WINDOW_MS = Long.getLong("com.zaxxer.hikari.aliveBypassWindowMs", MILLISECONDS.toMillis(500));,它看起来像是一个配置项,默认值是 500 毫秒。这个配置你要是从文档里找的话,是没有的,因为这个配置作者没有透出给用户使用。但是你要是配置了,是管用的,只是作者不建议用户修改,所以不透出。它是什么呢?既然跟检查连接要同时成立,随便猜猜也知道跟它有关。不卖关子,它是检查连接是否活着的空窗期,也就是说,如果这个连接从上次使用到现在,不到 500 毫秒,就不检查它是否活着了,默认它活着;超过 500 毫秒,才检查一下。

看起来又是一个优化点对吧?是的,是一个优化点。因为检查连接是否还存活,是比较耗时的,要使用该连接跟数据库通信一次。

有两种通信方式:

  1. JDBC4 以下版本的驱动,使用用户配置的connectionTestQuery中的 sql 来检查。

connectionTestQuery是获取连接的时候,用于检查连接是否可用的一个 sql,大家可能用过,常见的是配置一个select 1

  1. JDBC4 以上,如果不配置connectionTestQuery, 默认使用 ping 命令检查。

如果使用的是 JDBC4 以上的驱动,建议大家不用配置connectionTestQuery,因为 ping 命令的方式比执行一个 sql 要高效很多。

不管是使用较慢的执行 sql 检查还是 较快的ping 命令检查,这都是一个耗时操作,所以作者设置了一个空窗期,不需要每次获取连接都检查,500毫秒内用过该连接,那么连接还正常的可能性极大,就不检查了,提高性能。

后面closeConnection我们先不说,后面的文章统一分析连接关闭。

④连接可用

代码语言:java复制
//④
//记录连接借用
metricsTracker.recordBorrowStats(poolEntry, startTime);
//创建ProxyConnection, ProxyConnection是Connection的包装, 同时也创建一个泄露检测的定时任务
return poolEntry.createProxyConnection(leakTask.schedule(poolEntry), now);

如果第 3 步的检查全部通过,也就是拿到的连接是可用的,我们就要执行第 4 步了。

  • 上报监控平台

metricsTracker这一句,其实是记录连接的借用,不是我们通常使用的打印一下日志,而是上报给监控平台,HikariCP 是支持对接监控平台的。这里大家先知道这个逻辑,后面我们统一分析上报监控平台。

  • 为什么用代理连接?

最主要的就是return 的这一句代码了吧。我们说过poolEntry是底层数据库连接的一个包装类,代表一个数据库连接。那么从createProxyConnection字面来看,这个方法并不是直接返回数据库连接给用户使用,而是创建了一个代理连接,这个代理连接是什么?为什么不直接返回数据库连接给用户使用?

不管我们使用 Spring 还是自己写的代码从 HikariCP 连接池里拿连接,都是拿到一个java.sql.Connection类型的对象没错吧?它是一个 java 统一的数据库连接接口,不管你使用的是 mysql 还是oracle 等数据库,都是统一对接这个接口,都必须返回一个这个类型的连接给用户使用,相当于一个门面模式的设计,这样用户可以不理会底层使用什么数据库,代码都是一个样的。既然如此,HikariCP应该直接返回一个java.sql.Connection对吧?

没有那么简单。试想一下,假如 HikariCP 直接返回底层的数据库连接给用户使用,那么,如果用户自己关闭了这个底层数据库连接呢?那么这个连接在连接池里相当于已经不可用了,其他线程也使用不了了。作为一个框架设计者,不能指望每个用户都是高手,他们都能在用完数据库连接不会关闭它并且要还回连接池中,肯定有小白用户或者很唬的不管三七二十一的人。更何况除了关闭连接,还有你修改了连接的设置呢,比如自动提交事务,连接只读这些设置,然后没有恢复回原来的设置怎么办?如此混乱的话,我们使用连接池就没有意义了。所以我们不能把底层数据库连接直接给用户使用,这个大家理解了吧?

如何来实现呢?我们可以继承java.sql.Connection,创建一个它的子类,子类可以直接当做父类来用,没错吧?然后我们在子类里覆盖java.sql.Connection里面敏感的操作,比如关闭连接,如果用户调用了关闭连接操作,不是真正的关闭底层连接,而是将连接还回到连接池。怎么样?我们解决了用户瞎用的问题了吧。作者就是这个目的,才设计了一个createProxyConnection方法来创建了一个连接的代理ProxyConnection,将这个代理返回给用户使用。一切如我们所说的,ProxyConnection继承了java.sql.Connection,覆盖了一些方法,详细的我们后面单独的文章解析,这里很重要。

  • 泄露检测

我之前写过一个连接泄露检测的文章,是我写的浏览量最大的文章,这说明,有不少人都遇到这个问题。在 HikariCP 检测到连接泄露的时候,会抛出一个 warn:java.lang.Exception: Apparent connection leak detected。我们在这里详细说一下这个地方的逻辑。

  1. 连接泄露检测的相关配置

有一个leakDetectionThreshold的配置,这个就是连接泄露检测的最大时间,默认是 0,表示不启用泄露检测;最小值 2000 毫秒,如果用户设置的小于 2000 毫秒,默认关闭泄露检测,最大值不能超过连接的最大存活时间,也就是maxLifetime配置,超过的话也会自动禁用泄露检测。

  1. 泄露检测的定时任务

createProxyConnection方法中,我们可以看到传了一个参数leakTask.schedule(poolEntry)leakTask的类型是ProxyLeakTask,它实现了Runnable接口,是一个多线程的定时任务实现。它的内部持有几个成员变量:ScheduledExecutorService,是用来执行泄露检测定时任务的线程池;leakDetectionThreshold,是泄露检测超时时间;

scheduledFuture是任务的 future 结果,可以用来取消定时任务。

我们看下它的schedule方法:

代码语言:java复制
ProxyLeakTask schedule(final PoolEntry bagEntry) {
      return (leakDetectionThreshold == 0) ? NO_LEAK : new ProxyLeakTask(this, bagEntry);
   }

这里判断了下用户有没有开启泄露检测功能,如果是没有开启,那么就返回一个NO_LEAK。大家还记得FAUX_LOCK吧?就是上面的①处令牌桶的实现,是提供了一个空实现对吧?这里也是同样的道理,NO_LEAK是一个空实现,如果用户没有开启泄露检测就方便 JIT 把这段逻辑优化掉。

OK,我们看下new ProxyLeakTask(this, bagEntry)的实现:

代码语言:java复制
private ProxyLeakTask(final ProxyLeakTask parent, final PoolEntry poolEntry) {
      this.exception = new Exception("Apparent connection leak detected");
      this.connectionName = poolEntry.connection.toString();
      scheduledFuture = parent.executorService.schedule(this, parent.leakDetectionThreshold, TimeUnit.MILLISECONDS);
}

大家仔细观察下这个构造方法,第一个参数也是一个ProxyLeakTask,看名字parent是个父任务。这个父任务在连接池初始化的时候会创建,创建的时候需要两个参数,一个是用于执行任务的线程池executorService,另一个是连接泄露超时时间leakDetectionThreshold。此处传递父任务进来就是要使用父任务中的线程池和连接泄露超时时间。

我们看下超时检测的任务实现:

代码语言:java复制
public void run() {
      final StackTraceElement[] stackTrace = exception.getStackTrace();
      final StackTraceElement[] trace = new StackTraceElement[stackTrace.length - 5];
      System.arraycopy(stackTrace, 5, trace, 0, trace.length);

      exception.setStackTrace(trace);
      LOGGER.warn("Connection leak detection triggered for {}, stack trace follows", connectionName, exception);
   }

由于这里不太重要,我们就不一句一句的分析了,整个run方法就是构造一个异常,然后抛出一个 warn 异常栈。

到此,我们整个连接泄露的分析就结束了。

  • 释放锁

有一个需要注意的是,我们在最开始的第一句,是申请了一个令牌,现在上面已经获取到了可用连接,我们需要释放这个令牌。我们在使用其他锁的时候也是一样的,一定要在最后释放锁,为了防止任何异常打断代码执行,所以释放锁的代码一定要放在 finally 中,保证最后一定会把锁释放掉。

⑤获取连接超时

上面整个获取连接的过程②③④代码是放在 do-while 中来执行的,只要不超过设置的connectionTimeout,就会一直尝试循环获取连接,直到超过了connectionTimeout,就会执行⑤的代码。超时之后有两个步骤:一是向监控平台上报获取连接超时;二是构造一个异常信息,然后抛出去。

至此,整个获取连接的逻辑就介绍完了,可能有一些没有说到的细节,大家可以发表意见,我们一起学习讨论。

0 人点赞