关于数据库连接池:连接池-HikariPool-一-基础框架及初始化过程

51次阅读

共计 9458 个字符,预计需要花费 24 分钟才能阅读完成。

后面做了 n 多筹备,包含同步队列、阻塞队列、线程池、周期性线程池等等,明天终于能够开始深入研究连接池了,从 HikariPool 开始。

连接池存在的起因和线程池大略相似,因为数据库连贯的获取是有开销的,频繁获取、敞开数据库连贯会带来不必要的开销,影响零碎性能。所以就有了数据库连接池。

数据库连接池技术提前创立好数据库连贯,并对数据库连贯进行池化治理:连贯创立好之后交给连接池,利用须要连贯的时候间接从连接池获取、而不须要创立连贯,应用完之后归还给连接池、而不是敞开连贯。从而缩小频繁创立、敞开数据库连贯锁带来的开销,极大进步零碎性能。

明天从 HikariPool 开始,从源码角度学习理解数据库连接池的实现原理。HikariPool 是目前公认性能最好的数据库连接池,同时也是 SpringBoot2.0 当前的默认连接池,置信今后会有越来越多的公司和我的项目应用 HikariPool 连接池。

对于数据库连接池的根本认知

先对数据库连接池的根本工作原理做个理解,不论是 HikariPool、还是 druid,所有的数据库连接池应该都是依照这个基本原理工作和实现的,带着这个思路去学习数据库连接池,防止盲人摸象。

数据库连接池肯定会蕴含以下根本逻辑:

  1. 创立连贯并池化:初始化的时候创立、或者是在利用获取连贯的过程中创立,连贯创立好之后放在连接池(内存中的容器,比方 List)中保留。
  2. 获取数据库连贯:接管了获取数据库连贯的办法,从连接池中获取、而不是创立连贯。
  3. 敞开数据库连贯:接管了敞开数据库连贯的办法,将连贯偿还到连接池、而不是真正的敞开数据库连贯。
  4. 连接池保护:连接池容量、连贯超时清理等工作。

带着这个思路钻研 HikariPool 的源码,会有事倍功半的效用。

意识 HikariPool 的构造

包含以下几个局部:

  1. ConcurrentBag & PoolEntry
  2. addConnectionExecutor
  3. houseKeeperTask
  4. HikariProxyConnection
  5. closeConnectionExecutor

ConcurrentBag

直译就是“连贯口袋”,就是放数据库连贯的口袋,也就是连接池。

ConcurrentBag 有 3 个保留数据库连贯的中央(池):

  1. threadList:是一个 ThreadLocal 变量,保留以后线程持有的数据库连贯。
  2. sharedList:是一个 CopyOnWriteArrayList,线程平安的 arrayList,数据库连贯创立后首先进入 sharedList。
  3. handoffQueue:是一个 SynchronousQueue,数据库连贯创立、进入 sharedList 后,也会进入 handoffQueue 期待连贯申请。

除此之外,ConcurrentBag 还有一个比拟重要的属性 waiters,记录向连接池获取数据库连贯而不得、期待的线程数。

PoolEntry

连接池中存储的对象不是 Connection,也不是 Connection 的代理对象,而是 PoolEntry。

PoolEntry 持有数据库连贯对象 Connection,Connection 在实例化 PoolEntry 前创立。PoolEntry 创立的过程中会同时创立两个 ScheduledFuture 工作:endOfLife 和 keepalive。

endOfLife 提交 MaxLifetimeTask 定时工作 ,在 PoolEntry 创立后的 maxLifetime(参数指定)执行。MaxLifetimeTask 会敞开以后连贯、同时依据须要创立新的连贯退出到连接池。

keepalive 提交 KeepaliveTask 周期性工作 ,在 PoolEntry 创立后依照 keepalive(参数设定)周期性执行,在以后连贯闲暇的状况下查看连贯是否可用(应用参数指定的 ConnectionTestQuery 进行测试),如果连贯不可用则敞开并从新创立连贯。

addConnectionExecutor

增加数据库连贯的工作管理器。负责数据库连贯的创立以及退出到连接池中(sharedList 和 handoffQueue)。

留神 addConnectionExecutor 自身是一个线程池 ThreadPoolExecutor,线程池容量为最大数据库连接数,HikariPool 初始化的过程中会以多线程(corePoolSize=CPU 内核数量)的形式疾速实现连贯创立和池化,之后失常状况下会以单线程(corePoolSize=1)的形式创立数据库连贯。

houseKeeperTask

是一个周期性线程池 ScheduledExecutorService,初始化的时候创立,定时(housekeepingPeriodMs)执行,清理掉(敞开)连接池中多余的(大于最小闲暇数)闲暇连贯,如果连接池中的连接数没有达到参数设置的数量(最大连接数、或者闲暇连贯没有达到最小闲暇连接数)则创立连贯。

HikariProxyConnection

HikariProxyConnection 是数据库连贯 Connection 的扩大类,应用层通过 getConnection 办法获取到的数据库连贯其实不是不是数据库连贯 Connection、而是这个扩大类 HikariProxyConnection。

应用扩大类 HikariProxyConnection、而不是原生的数据库连贯 Connection 的一个最直观的理由是,对于数据库连接池来说,连贯的敞开办法 close 不是要敞开连贯、而是要把连贯交还给连接池。应用扩大类(或者代理类)就能够很容易的重写其 close 办法实现目标,而如果间接应用原生 Connection 的话,就没方法管制了。

closeConnectionExecutor

数据库连接池须要敞开的时候,通过 closeConnectionExecutor 线程池提交,敞开连贯后 closeConnectionExecutor 还负责调用 fillPool(),依据须要填充连接池。

好了,HikariPool 的根底构造理解完了,接下来要进入源码剖析了,次要包含:

  1. HikariPool 的初始化
  2. 获取数据库连贯 – getConnection 办法
  3. 敞开数据库连贯 – close 办法

HikariPool 的初始化

要开始剖析源码了。

HikariPool 是实例化的时候在构造方法中实现初始化。代码尽管不是很多,然而内容很多,所以还是分段、一步一步剖析。

 public HikariPool(final HikariConfig config)
   {super(config);

      this.connectionBag = new ConcurrentBag<>(this);
      this.suspendResumeLock = config.isAllowPoolSuspension() ? new SuspendResumeLock() : SuspendResumeLock.FAUX_LOCK;

      this.houseKeepingExecutorService = initializeHouseKeepingExecutorService();

      checkFailFast();

首先调用 super 也就是 PoolBase 的构造方法,次要设置配置的相干参数,初始化 DataSource(initializeDataSource,获取到数据库连贯的 url、username、password 等重要参数,创立 DataSource)。

之后创立 ConcurrentBag、实现 ConcurrentBag 的初始化(通过构造方法):创立 handoffQueue、sharedList 以及 threadList。

接下来初始化 houseKeepingExecutorService,也就是创立一个 ScheduledThreadPoolExecutor,筹备接管 houseKeep 工作。

而后调用 checkFailFast(),checkFailFast() 的作用是在初始化连接池之前首先做一次疾速尝试:创立一个 PoolEntry( 通过 createPoolEntry 办法,稍后剖析源码 ),如果创立胜利则将其退出连接池后,返回,否则如果失败(数据库连贯创立失败)则一直尝试直到消耗完设置的初始化工夫 initializationTimeout。

接着看初始化代码:

      if (config.getMetricsTrackerFactory() != null) {setMetricsTrackerFactory(config.getMetricsTrackerFactory());
      }
      else {setMetricRegistry(config.getMetricRegistry());
      }
     setHealthCheckRegistry(config.getHealthCheckRegistry());

      handleMBeans(this, true);

      ThreadFactory threadFactory = config.getThreadFactory();

      final int maxPoolSize = config.getMaximumPoolSize();
      LinkedBlockingQueue<Runnable> addConnectionQueue = new LinkedBlockingQueue<>(maxPoolSize);
      this.addConnectionQueueReadOnlyView = unmodifiableCollection(addConnectionQueue);
      this.addConnectionExecutor = createThreadPoolExecutor(addConnectionQueue, poolName + "connection adder", threadFactory, new ThreadPoolExecutor.DiscardOldestPolicy());
      this.closeConnectionExecutor = createThreadPoolExecutor(maxPoolSize, poolName + "connection closer", threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());

      this.leakTaskFactory = new ProxyLeakTaskFactory(config.getLeakDetectionThreshold(), houseKeepingExecutorService);

      this.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, housekeepingPeriodMs, MILLISECONDS);

以上代码次要实现以下几件事件:
MetricsTrackerFactory 次要用来创立数据库连贯的统计分析工作。

注册健康检查工作,默认是 CodahaleHealthChecker,负责连贯的健康检查,明天暂不波及。

JMX 解决,暂不波及。

最要害的局部来了: 创立 addConnectionExecutor,addConnectionQueueReadOnlyView,closeConnectionExecutor,leakTaskFactory 以及 houseKeeperTask。

这几个线程池都十分重要。

addConnectionExecutor 负责创立连贯、封装到 PoolEntry 中之后退出连接池中。houseKeeperTask 负责连接池清理、连接池中的连贯数量没有达到参数设置的连贯数量的话,houseKeeperTask 还负责创立连贯。leakTaskFactory 负责创立连贯泄露查看工作。

HikariPool 连接池初始化的大部分工作都在以上几行代码中:

首先来看 houseKeeperTask,HouseKeeperTask 提交 HouseKeeper 工作,HouseKeeper 是实现了 Runnable 接口的 HikariPool 的外部类,HouseKeeper 查看以后闲暇连接数如果大于参数设置的最小闲暇连接数的话,会把超过 idleTimeout 未用的连贯敞开。之后调用 fillPool() 办法。

fillPool() 办法查看以后连接池中的连接数没有达到参数设置的最大连接数、或者闲暇连接数没有达到参数设置的最小闲暇连接数的话, 通过调用 addConnectionExecutor.submit 办法提交 poolEntryCreator 工作、创立 n 个连贯(并退出连接池)直到连贯数量满足参数设置的要求。

PoolEntryCreator 是实现了 callable 接口的工作,提交给线程池 addConnectionExecutor 之后,addConnectionExecutor 会调用 PoolEntryCreator 的 call() 办法,call()办法调用 createPoolEntry() 办法创立数据库连贯、创立之后调用 connectionBag.add 办法将新创建的 PoolEntry 退出连接池。

代码看到这里,咱们能够得出一个论断:houseKeeperTask 会通过 addConnectionExecutor 提交多个(参数最大连接数、最小闲暇连接数指定)创立数据库连贯的工作,从而实现数据库连接池中初始化连贯的创立!

接下来,咱们再看一下初始化的最初一段代码:

      if (Boolean.getBoolean("com.zaxxer.hikari.blockUntilFilled") && config.getInitializationFailTimeout() > 1) {addConnectionExecutor.setMaximumPoolSize(Math.min(16, Runtime.getRuntime().availableProcessors()));
         addConnectionExecutor.setCorePoolSize(Math.min(16, Runtime.getRuntime().availableProcessors()));

         final long startTime = currentTime();
         while (elapsedMillis(startTime) < config.getInitializationFailTimeout() && getTotalConnections() < config.getMinimumIdle()) {quietlySleep(MILLISECONDS.toMillis(100));
         }

         addConnectionExecutor.setCorePoolSize(1);
         addConnectionExecutor.setMaximumPoolSize(1);
      }

这段代码的意思是:如果参数 blockUntilFilled 设置为 true(连接池没有实现初始化则阻塞)、并且参数 InitializationFailTimeout>1(初始化阻塞时长)的话,则设置 addConnectionExecutor 的最大线程数和外围线程数为 16 和内核数量的最大值,阻塞期待初始化时长达到参数设定值、或者连接池中创立的连贯数量曾经大于最小闲暇数量后,从新设置 addConnectionExecutor 的外围线程数和最大线程数为 1。

这段代码要达到的指标是:在参数设定的初始化时长范畴内,将 addConnectionExecutor 线程池加大到最大马力(线程数设置到尽可能最大)、以最短的工夫实现初始连贯的创立。初始连贯创立实现之后,将 addConnectionExecutor 的线程数复原为 1, 也就是说初始化的时候调动尽可能多的线程创立连贯、初始化实现之后的连贯创立实际上是由一个线程实现的。

初始化的代码剖析结束,然而为了可读性,咱们跳过了 createPoolEntry 办法的源码,创立物理连贯就是在 createPoolEntry 办法实现的,所以 createPoolEntry 办法也十分重要。

当初补上!

createPoolEntry 办法

createPoolEntry 是 HikariPool 连接池中惟一创立数据库连贯的中央,通过线程池 addConnectionExecutor 绑定的 PoolEntryCreator 发动调用。

private PoolEntry createPoolEntry()
   {
      try {final PoolEntry poolEntry = newPoolEntry();

         final long maxLifetime = config.getMaxLifetime();
         if (maxLifetime > 0) {
            // variance up to 2.5% of the maxlifetime
            final long variance = maxLifetime > 10_000 ? ThreadLocalRandom.current().nextLong( maxLifetime / 40) : 0;
            final long lifetime = maxLifetime - variance;
            poolEntry.setFutureEol(houseKeepingExecutorService.schedule(new MaxLifetimeTask(poolEntry), lifetime, MILLISECONDS));
         }

         final long keepaliveTime = config.getKeepaliveTime();
         if (keepaliveTime > 0) {
            // variance up to 10% of the heartbeat time
            final long variance = ThreadLocalRandom.current().nextLong(keepaliveTime / 10);
            final long heartbeatTime = keepaliveTime - variance;
            poolEntry.setKeepalive(houseKeepingExecutorService.scheduleWithFixedDelay(new KeepaliveTask(poolEntry), heartbeatTime, heartbeatTime, MILLISECONDS));
         }

         return poolEntry;
      }
      catch (ConnectionSetupException e) {if (poolState == POOL_NORMAL) {// we check POOL_NORMAL to avoid a flood of messages if shutdown() is running concurrently
            logger.error("{} - Error thrown while acquiring connection from data source", poolName, e.getCause());
            lastConnectionFailure.set(e);
         }
      }
      catch (Exception e) {if (poolState == POOL_NORMAL) {// we check POOL_NORMAL to avoid a flood of messages if shutdown() is running concurrently
            logger.debug("{} - Cannot acquire connection from data source", poolName, e);
         }
      }

      return null;
   }

首先创立 PoolEntry,而后通过线程池 houseKeepingExecutorService 绑定 endOfLife 以及 keepalive 工作。这两个工作的作用请参考本文的 PoolEntry 局部的形容。

咱们还是没有看到创立数据库物理连贯的中央,别急,他就在办法的第一行代码:newPoolEntry() 中,咱们看一下 newPoolEntry 办法。

  PoolEntry newPoolEntry() throws Exception
   {return new PoolEntry(newConnection(), this, isReadOnly, isAutoCommit);
   }

创立数据库物理连贯,交给 PoolEntry 的构造方法去创立 PoolEntry,最终 PoolEntry 对象会持有创立的数据库连贯。

PoolEntry 创立好之后,会调用 connectionBag.add 办法将其退出的连接池中。

ConcurrentBag#add

线程池 addConnectionExecutor 创立 PoolEntry 之后,会调用 ConcurrentBag 的 add 办法将其退出到连接池中:

public void add(final T bagEntry)
   {if (closed) {LOGGER.info("ConcurrentBag has been closed, ignoring add()");
         throw new IllegalStateException("ConcurrentBag has been closed, ignoring add()");
      }

      sharedList.add(bagEntry);

      // spin until a thread takes it or none are waiting
      while (waiters.get() > 0 && bagEntry.getState() == STATE_NOT_IN_USE && !handoffQueue.offer(bagEntry)) {Thread.yield();
      }
   }

首先退出到 sharedList 中。

之后如果连接池有期待获取连贯的线程、并且以后连贯处于闲暇状态,则提交以后 PoolEntry 给 handoffQueue 队列。

handoffQueue 是手递手队列,如果以后工夫点并没有排队想要从 handoffQueue 获取连贯的线程存在的话,以后线程会挂起期待。这里所说的“以后线程”就是“创立数据库连贯”的线程,是 addConnectionExecutor 线程池中的线程。

addConnectionExecutor 中的线程在创立数据库连贯 poolEntry 之后,有以下可能:

  1. 以后正好有利用须要获取数据库连贯,并且通过 thredList 和 shareList 都没有获取到连贯、须要到 handoffQueue 获取的话,则以后刚创立的数据库连贯通过 handoffQueue 交给获取线程后,以后线程偿还到 addConnectionExecutor 线程池中,如果有多个 poolEntry 在 handoffQueue 排队的话,以后线程 yield 期待
  2. 以后没有须要排队获取连贯的线程(waiter=0), 则 poolEntry 退出到 shareList 中之后,以后线程间接偿还到 addConnectionExecutor 线程池中

小结

HikariPool 的根本框架以及初始化过程、数据库连贯的创立以及退出池中、连接池的 houseKeep 过程的源码剖析结束。剩下的就是应用层从池中获取连贯、以及敞开连贯的逻辑了,下一篇文章剖析。

Thanks a lot!

上一篇 Java 并发编程 Lock Condition & ReentrantLock(二)

正文完
 0