前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >二、HikariCP获取连接流程源码分析二

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

原创
作者头像
用户1422411
发布2022-06-25 17:49:43
7370
发布2022-06-25 17:49:43
举报
文章被收录于专栏:HikariCP源码解析系列

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

源代码版本2.4.5-SNAPSHOT

HikariPool的getConnection()方法

在上一篇《HikariCP获取连接流程源码分析一》中,我们分析了HikariDataSource的getConnection()方法,而这个方法,其实详细的实现细节都是在HikariPool的getConnection()方法中,我们来分析下HikariPool的getConnection()方法。

代码如下:

代码语言:java
复制
public final Connection getConnection() throws SQLException {
      return getConnection(connectionTimeout);
   }

这里又调用了一个有参的getConnection()方法,但是我们并没有传参数connectionTimeout,这个是哪里来的呢?这个其实就是用户在初始化连接池的时候设置的参数connectionTimeout,它表示获取连接的超时时间,不配置的话默认值 30秒。我们继续看下getConnection(connectionTimeout);的实现:

代码语言:java
复制
public final Connection getConnection(final long hardTimeout) throws SQLException {
      //①
      //获取连接的时候申请令牌, 主要是为了连接池挂起的时候, 控制用户不能获取连接
      //当连接池挂起的时候, Semaphore的 10000 个令牌都会被占用, 此处就会一直阻塞线程等待令牌
      suspendResumeLock.acquire();
      //记录获取连接的开始时间, 用于超时判断
      final long startTime = clockSource.currentTime();

      try {
         long timeout = hardTimeout;
         do {
            //②
            //从连接池获取连接, 超时时间timeout
            final PoolEntry poolEntry = connectionBag.borrow(timeout, MILLISECONDS);
            //borrow方法在超时的时候才会返回 null
            if (poolEntry == null) {
               break; // We timed out... break and throw exception
            }

            final long now = clockSource.currentTime();
            //③
            //获取连接的时候, 判断连接是否已经被标记移除
            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);
            } else {
               //④
               //记录连接借用
               metricsTracker.recordBorrowStats(poolEntry, startTime);
               //创建ProxyConnection, ProxyConnection是Connection的包装, 同时也创建一个泄露检测的定时任务
               return poolEntry.createProxyConnection(leakTask.schedule(poolEntry), now);
            }
         } while (timeout > 0L);
      } catch (InterruptedException e) {
         throw new SQLException(poolName + " - Interrupted during connection acquisition", e);
      } finally {
         //释放锁
         suspendResumeLock.release();
      }

      //⑤
      //获取连接超时才会执行下面的代码
      logPoolState("Timeout failure ");
      metricsTracker.recordConnectionTimeout();

      String sqlState = null;
      final Throwable originalException = getLastConnectionFailure();
      if (originalException instanceof SQLException) {
         sqlState = ((SQLException) originalException).getSQLState();
      }
      final SQLException connectionException = new SQLTransientConnectionException(poolName + " - Connection is not available, request timed out after " + clockSource.elapsedMillis(startTime) + "ms.", sqlState, originalException);
      if (originalException instanceof SQLException) {
         connectionException.setNextException((SQLException) originalException);
      }
      throw connectionException;
   }

①Semaphore

代码语言:java
复制
suspendResumeLock.acquire();
//记录获取连接的开始时间, 用于超时判断
final long startTime = clockSource.currentTime();

getConnection的第一步,首先是获取令牌。我们从变量的名字suspendResumeLock来看,可能跟挂起(suspend)有关,那么挂起什么东西?如果之前大家有读过 HikariCP 的文档,或者使用过HikariCP的挂起功能,那么你肯定已经猜到了,这个是跟挂起整个连接池有关。

  • 挂起HikariCP

HikariCP的挂起功能,其实就是暂停用户获取连接,也就是说,挂起整个连接池之后,如果有线程要从连接池获取连接,那么会一直阻塞,直到连接池被恢复。

挂起有什么用?

作者 brett 提到挂起的使用方法:

  1. 挂起连接池
  2. 更改数据库连接池配置,或者更改 DNS 配置(指向新的主服务器)
  3. 软驱逐连接池中现有的连接
  4. 恢复连接池

HikariCP可以在运行期通过 JMX修改一些配置的(并不是所有的配置), 有:connectionTimeout(获取连接的超时时间),leakDetectionThreshold(连接泄露检测时间),maxPoolSize(连接池最大连接数),minIdle(最小空闲连接数),maxLifetime(连接最大存活时间),idleTimeout(连接最大空闲时间),共 7 项。

比如我挂起了连接池,然后修改了maxLifetime,那么连接池中现有的连接还是之前的配置,我就要将所有的连接都从连接池中驱逐出去,然后恢复连接池,这时候连接池就会使用新的配置创建新的连接。

除此之外,还可以使用连接池挂起时,线程一直阻塞无法获取到连接这个特性,来模拟数据库连接故障,来测试应用。

  • 怎么实现的

OK,我们知道了这一句代码的目的主要是挂起连接池时,阻止用户获取连接的。那么是怎么实现的呢?

其实,suspendResumeLock的类是com.zaxxer.hikari.util.SuspendResumeLock,它的内部是用Semaphore实现的。Semaphore是 java 的concurrent包下的 并发工具类,它用给线程发放令牌的方式,控制线程的并发数量。

举个场景例子,假如是秒杀:我们知道服务器的最大并发处理能力是同时处理 1000 个请求,超过 1000 个请求服务器可能会宕机,在不扩容的情况下,尽量保证服务可用。这个时候,我们就要控制用户的请求数量不能超过 1000对吧。

这时候我们可以用Semaphore实现,Semaphore类似一个令牌桶,桶里可以放指定数量的令牌,在并发的时候,每个线程从桶里拿一个(也可以是多个)令牌,拿到令牌的才可以继续执行,拿不到令牌的就等着(根据策略不同,也可能是抛异常等),直到有其他线程释放令牌之后,得到令牌继续执行。

上面的场景,我们可以使用Semaphore初始化 1000 个令牌,每个线程拿一个令牌,这样我们就可以控制同时处理的请求数量不超过 1000了吧。

同样的道理,我们看下挂起连接池的方法:

代码语言:java
复制
public void suspend() {
      //MAX_PERMITS = 10000
      acquisitionSemaphore.acquireUninterruptibly(MAX_PERMITS);
   }

HikariCP 在这里初始化了 1万个令牌,如果用户调用了suspend()挂起连接池,其实就是调用了Semaphore一次获取 1 万个令牌,这样其他线程就没有令牌可以拿了,只能一直等,直到用户恢复线程池,释放这 1 万个令牌到桶里。

需要注意的是,要使用挂起连接池的功能,必须配置isAllowPoolSuspension=true,否则使用挂起功能会报错。

我们提到isAllowPoolSuspension其实是还要说一下suspendResumeLock的一个优化点。

在初始化连接池的时候,这个suspendResumeLock根据你是否开启了挂起功能,会有不同的实现,this.suspendResumeLock = config.isAllowPoolSuspension() ? new SuspendResumeLock() : SuspendResumeLock.FAUX_LOCK;

假如没有启用挂起功能,那么suspendResumeLock是一个FAUX_LOCK

FAUX_LOCK是什么呢?看代码:

代码语言:java
复制
public static final SuspendResumeLock FAUX_LOCK = new SuspendResumeLock(false) {
      @Override
      public void acquire() {}
      @Override
      public void release() {}
      @Override
      public void suspend() {}
      @Override
      public void resume() {}
   };

你没有看错,就是一个空实现,方法里什么都没有。

这么做有什么好处?其实是,当挂起功能没有开启的时候, 它会提供一个空实现, 希望 JIT 能将之优化掉。也就是说,每次申请令牌其实是调用空方法,什么都不干,代码在运行多次之后,JIT 有可能会把它优化掉,根本就不调用了。这样,我们每次获取连接的时候,会节省申请令牌的额外开销,提高性能。

最快的一般不是浪得虚名,肯定都有值得我们学习的地方......你学会了没有?

我们继续分析。

clockSource是一个时间的工具类,用于获取当前时间,计算时间差等等。此处记录了当前时间,用于后面时间差计算,判断获取连接是否超时用的。

②获取连接

代码语言:java
复制
//②
//从连接池获取连接, 超时时间timeout
final PoolEntry poolEntry = connectionBag.borrow(timeout, MILLISECONDS);
//borrow方法在超时的时候才会返回 null
if (poolEntry == null) {
  break; // We timed out... break and throw exception
}

此处的代码,我们可以看到,从connectionBag中获取了一个poolEntry对象。poolEntry其实是对数据库连接的一个包装类,connectionBag才是 HikariCP中实际保存数据库连接的容器,里面是一个CopyOnWriteArrayList。由于connectionBag非常重要,我们要在后面单独分析,此处不深入进去了。

但是从connectionBag获取连接的时候,我可以看到传了一个参数timeout,这个timeout就是我们配置的connectionTimeout,获取连接的超时时间,如果在指定的timeout时间内,没有返回一个连接,那么就返回一个 null。

此时已经超时了,所以下面的判断就是跳出循环,不在尝试获取连接了。

由于放在一篇文章里太长,未尽事宜,下篇继续!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • HikariPool的getConnection()方法
  • ①Semaphore
  • ②获取连接
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档