敞开成长之旅!这是我参加「日新计划 2 月更文挑战」的第 8 天,点击查看活动概况

导言

Springboot 2.0将 HikariCP 作为默认数据库衔接池这一事情之后,HikariCP 作为一个后起之秀出现在群众的视界中。HikariCP 是在日本的程序员开源的,hikari日语意思为“光”,HikariCP 也以速度快的特点遭到越来越多人的青睐。

今日就让咱们来探讨一下HikariCP为什么这么快?

衔接池技能

咱们往常编码过程中,经常会碰到线程池啊,数据库衔接池啊等等,那么这个池到底是一门怎样的技能呢?

简略来说,衔接池是一个创立和办理衔接的缓冲池技能。衔接池首要由三部分组成:衔接池的树立、衔接池中衔接的运用办理、衔接池的封闭。衔接池技能的核心思想是:衔接复用,经过树立一个数据库衔接池以及一套衔接运用、分配、办理战略,使得该衔接池中的衔接能够得到高效、安全的复用。它不只是只限于办理数据库访问衔接,也能够办理其他衔接资源。

HakariCP

HakariCP 项目的 README 中的一段话。

Fast, simple, reliable. HikariCP is a “zero-overhead” production ready JDBC connection pool. At roughly 130Kb, the library is very light.

快速、简略、牢靠。HikariCP是一个“零开支”的出产就绪JDBC衔接池。这个库大约有130Kb,非常轻。

这个介绍真是简洁但“全面”。搭配上下边这张图。

“光”速?HakariCP为什么这么快?

看到这些数据,再加上Springboot 2.0 将 HikariCP 作为默认数据库衔接池这件事,我已经非常好奇 HikariCP 的完成原理了。

HikariCP为什么这么快?

  • 两个HikariPool:界说了两个HikariPool目标,一个选用final类型界说,防止在获取衔接时才初始化,提高功能,也防止volatile的额外开支。
  • FastList代替ArrayList:选用自界说的FastList代替了ArrayList,FastList的get办法去除了规模查看逻辑,而且remove办法是从尾部开端扫描的,而并不是从头部开端扫描的。由于Connection的打开和封闭次序通常是相反的。
  • 更快的并发调集完成:运用自界说ConcurrentBag,功能更优。
  • 更快的获取衔接:同一个线程获取数据库衔接时从ThreadLocal中获取,没有并发操作。
  • 精简字节码:HikariCP利用了一个第三方的Java字节码修正类库Javassist来生成托付完成动态署理,速度更快,相比于JDK 署理生成的字节码更少。

HikariCP原理

咱们经过剖析源码来看 HikariCP 是怎么这么快的。先来看一下 HikariCP 的简略运用。

maven依靠:

<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>4.0.3</version>
</dependency>
@Test
public void testHikariCP() throws SQLException {
    // 1、创立Hikari装备
    HikariConfig hikariConfig = new HikariConfig();
    // JDBC衔接串
    hikariConfig.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/iam?characterEncoding=utf8");
    // 数据库用户名
    hikariConfig.setUsername("root");
    // 数据库用户暗码
    hikariConfig.setPassword("123456");
    // 衔接池称号
    hikariConfig.setPoolName("testHikari");
    // 衔接池中最小闲暇衔接数量
    hikariConfig.setMinimumIdle(4);
    // 衔接池中最大闲暇衔接数量
    hikariConfig.setMaximumPoolSize(8);
    // 衔接在池中的最大闲暇时刻
    hikariConfig.setIdleTimeout(600000L);
    // 数据库衔接超时时刻
    hikariConfig.setConnectionTimeout(10000L);
    // 2、创立数据源
    HikariDataSource dataSource = new HikariDataSource(hikariConfig);
    // 3、获取衔接
    Connection connection = dataSource.getConnection();
    // 4、获取Statement
    Statement statement = connection.createStatement();
    // 5、履行Sql
    ResultSet resultSet = statement.executeQuery("SELECT COUNT(*) AS countNum tt_user");
    // 6、输出履行成果
    if (resultSet.next()) {
        System.out.println("countNum成果为:" + resultSet.getInt("countNum"));
    }
    // 7、开释链接
    resultSet.close();
    statement.close();
    connection.close();
    dataSource.close();
}

HikariConfig:能够设置一些数据库根本装备信息和一些衔接池的装备信息。

HikariDataSource:完成了 DataSource,DataSource是一个数据源标准或者说标准,Java一切衔接池需求基于这个标准进行完成。

咱们就从 HikariDataSource 开端说起。HikariDataSource有两个结构办法HikariDataSource()HikariDataSource(HikariConfig configuration)

private final HikariPool fastPathPool;
private volatile HikariPool pool;
public HikariDataSource()
{
   super();
   fastPathPool = null;
}
public HikariDataSource(HikariConfig configuration)
{
   configuration.validate();
   configuration.copyStateTo(this);
   LOGGER.info("{} - Starting...", configuration.getPoolName());
   pool = fastPathPool = new HikariPool(this);
   LOGGER.info("{} - Start completed.", configuration.getPoolName());
   this.seal();
}

HikariPool为什么要有两个(fastPathPool和pool)呢?

能够看到无参结构办法fastPathPool是null,有参结构pool = fastPathPool,选用无参结构在getConnection()时候才会初始化(下边会详细解说),功能略低,而且pool是volatile要害字润饰,会有一些额外开支。所以主张运用有参结构。这也是HikariPool快的原因之一。

有参结构里有一行new HikariPool(this),咱们来看一下怎样个事。

代码太多了,往后只贴要害代码了。。。

public HikariPool(final HikariConfig config)
{
   super(config);
   // 初始化ConcurrentBag目标
   this.connectionBag = new ConcurrentBag<>(this);
   // 创立SuspendResumeLock目标 
   this.suspendResumeLock = config.isAllowPoolSuspension() ? new SuspendResumeLock() : SuspendResumeLock.FAUX_LOCK;
   // 依据装备的最大衔接数,创立链表类型堵塞行列
   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.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, housekeepingPeriodMs, MILLISECONDS);
   ...
}

HikariPool 是为HikariCP提供根本池行为的首要衔接池类。

houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, housekeepingPeriodMs, MILLISECONDS)这行代码是 创立坚持衔接池衔接数量的使命。该使命会封闭需求被丢弃的衔接,确保最小衔接数,HouseKeeper类的run()办法中有一行代码fillPool()会创立衔接,咱们来看一下。

创立衔接

private synchronized void fillPool()
{
    // 核算需求增加的衔接数量
    final int connectionsToAdd = Math.min(config.getMaximumPoolSize() - getTotalConnections(), config.getMinimumIdle() - getIdleConnections()) - addConnectionQueue.size();
    for (int i = 0; i < connectionsToAdd; i++) {
        // 向创立衔接线程池中提交创立衔接的使命
        addConnectionExecutor.submit((i < connectionsToAdd - 1) ? poolEntryCreator : postFillPoolEntryCreator);
    }
    ...
}

来看一下PoolEntryCreator是怎么创立衔接的。

@Override
public Boolean call()
{
   // 衔接池状况正常而且需求创立衔接时
   while (poolState == POOL_NORMAL && shouldCreateAnotherConnection()) {
      // 创立PoolEntry目标
      final PoolEntry poolEntry = createPoolEntry();
      if (poolEntry != null) {
         // 将PoolEntry目标增加到ConcurrentBag目标中的sharedList中
         connectionBag.add(poolEntry);
         return Boolean.TRUE;
      }
   }
   ...
   return Boolean.FALSE;
}

PoolEntryCreator完成了Callable接口,在call()办法里能够看到创立衔接的过程。来持续看一下createPoolEntry()办法。

 private PoolEntry createPoolEntry()
{
    // 初始化PoolEntry目标
    final PoolEntry poolEntry = newPoolEntry();
    ...
}

持续进入newPoolEntry()办法。

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

PoolEntry结构时会先创立Connection目标传入结构函数中。PoolEntry是ConcurrentBag实例中用来盯梢Connection的。

获取链接

获取链接是经过getConnection()办法获取的,源码如下。

public Connection getConnection() throws SQLException
{
   if (isClosed()) {
      throw new SQLException("HikariDataSource " + this + " has been closed.");
   }
   if (fastPathPool != null) {
      return fastPathPool.getConnection();
   }
   HikariPool result = pool;
   if (result == null) {
      synchronized (this) {
         result = pool;
         if (result == null) {
            validate();
            LOGGER.info("{} - Starting...", getPoolName());
            try {
               pool = result = new HikariPool(this);
               this.seal();
            }
            catch (PoolInitializationException pie) {
               if (pie.getCause() instanceof SQLException) {
                  throw (SQLException) pie.getCause();
               }
               else {
                  throw pie;
               }
            }
            LOGGER.info("{} - Start completed.", getPoolName());
         }
      }
   }

会先去fastPathPool获取衔接,假如fastPathPool为null,就会经过pool获取,假如pool也为null,会经过 双检 代码来初始化线程池。

这个上文说到过为什么两个HikariPool,fastPathPool是 final 润饰的,而pool是 volatile 润饰的,这就阐明fastPathPool比pool功能更高,所以主张要用有参结构来创立HikariDataSource,才干享遭到这点小细节的优化。

持续进入HikariPool#getConnection(final long hardTimeout),办法中有一行要害的代码PoolEntry poolEntry = connectionBag.borrow(timeout, MILLISECONDS),这行代码的作用是从ConcurrentBag中借出一个PoolEntry目标。PoolEntry能够看作是对Connection目标的封装,衔接池中存储的衔接其实便是一个个的PoolEntry

这个connectionBag是用来做什么的呢?

ConcurrentBag

ConcurrentBag 是HikariCP自界说的一个无锁并发调集类。咱们接着来看一下 ConcurrentBag 的成员变量。

private final CopyOnWriteArrayList<T> sharedList;
private final boolean weakThreadLocals;
private final ThreadLocal<List<Object>> threadList;
private final IBagStateListener listener;
private final AtomicInteger waiters;
private volatile boolean closed;
private final SynchronousQueue<T> handoffQueue;
属性 类型 描述
sharedList CopyOnWriteArrayList 寄存 PoolEntry 目标
weakThreadLocals boolean 是否运用弱引证
threadList ThreadLocal 寄存当时线程的 PoolEntry 目标
listener IBagStateListener 增加元素的监听器
waiters AtomicInteger 当时等候的线程数
closed boolean 是否封闭
handoffQueue SynchronousQueue 公正形式行列,即先进先出

回到borrow()办法,看一下borrow的完成逻辑。

public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException
{
   // 从ThreadLocal中获取当时线程绑定的目标调集,存在则获取
   final List<Object> list = threadList.get();
   for (int i = list.size() - 1; i >= 0; i--) {
      final Object entry = list.remove(i);
      @SuppressWarnings("unchecked")
      final T bagEntry = weakThreadLocals ? ((WeakReference<T>) entry).get() : (T) entry;
      if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
         return bagEntry;
      }
   }
   // 等候目标加一
   final int waiting = waiters.incrementAndGet();
   try {
      // sharedList有未运用的则返回一个
      for (T bagEntry : sharedList) {
         if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
            // If we may have stolen another waiter's connection, request another bag add.
            if (waiting > 1) {
               listener.addBagItem(waiting - 1);
            }
            return bagEntry;
         }
      }
      // sharedList没有,增加一个监听使命
      listener.addBagItem(waiting);
      timeout = timeUnit.toNanos(timeout);
      do {
         final long start = currentTime();
         // 堵塞行列计时获取
         final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS);
         if (bagEntry == null || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
            return bagEntry;
         }
         timeout -= elapsedNanos(start);
      } while (timeout > 10_000);
      return null;
   }
   finally {
      // 等候线程数减一
      waiters.decrementAndGet();
   }
}
  1. 先从ThreadLocal中获取以前用过的衔接。ThreadLocal是当时线程的缓存,加速本地衔接获取速度。
  2. ThreadLocal中未获取到,会测验从sharedList中获取,sharedList调集存在初始化的PoolEntry。sharedList是CopyOnWriteArrayList类型的,写时仿制,特别合适这种读多写少的场景。
  3. sharedList中未获取到那就到堵塞行列中等着,看有没有偿还的衔接能够运用。

开释衔接

用完衔接后咱们要开释,经过connection.close()开释衔接,开释衔接时HakariCP也做到了一些巧妙的细节。ProxyConnection的close()办法是HakariCP开释衔接的完成逻辑。咱们知道衔接封闭前必须要封闭Statement,HakariCP对这里做了优化,来看一下代码完成。

private final FastList<Statement> openStatements;
private synchronized void closeStatements()
{
   final int size = openStatements.size();
   if (size > 0) {
      for (int i = 0; i < size && delegate != ClosedConnection.CLOSED_CONNECTION; i++) {
         try (Statement ignored = openStatements.get(i)) {
         }
         catch (SQLException e) {
            LOGGER.warn("{} - Connection {} marked as broken because of an exception closing open statements during Connection.close()",
                        poolEntry.getPoolName(), delegate);
            leakTask.cancel();
            poolEntry.evict("(exception closing Statements during Connection.close())");
            delegate = ClosedConnection.CLOSED_CONNECTION;
         }
      }
      openStatements.clear();
   }
}

存储Statement目标用的是 FastList,这也是 HakariCP 之所以快的原因之一。

为什么用FastList而不必ArrayList呢?

  • 去掉索引规模查看:查看源码会发现,FastList的get()办法比ArrayList少了一行代码rangeCheck(index),这行代码的作用是规模查看,少了这行代码必然会功能更优。不禁感叹真的是太细了啊,到处都是细节。
  • 尾部删去:FastList#remove()办法是从尾部开端扫描的,而并不是从头部开端扫描的。由于Connection的打开和封闭次序通常是相反的。FastList的依据下标删去办法也去掉索引规模查看。

封闭掉Statement之后,咱们再回过头来持续往下看。

poolEntry.recycle(lastAccess);

recycle办法会将该衔接偿还给线程池,recycle办法套了好几层,终究履行的是ConnectionBag的recycle办法,咱们直接进入看一下。

public void requite(final T bagEntry)
{
   bagEntry.setState(STATE_NOT_IN_USE);
   for (int i = 0; waiters.get() > 0; i++) {
      if (bagEntry.getState() != STATE_NOT_IN_USE || handoffQueue.offer(bagEntry)) {
         return;
      }
      else if ((i & 0xff) == 0xff) {
         parkNanos(MICROSECONDS.toNanos(10));
      }
      else {
         Thread.yield();
      }
   }
   final List<Object> threadLocalList = threadList.get();
   if (threadLocalList.size() < 50) {
      threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry);
   }
}

首先将状况设置为未运用,然后判断当时是否存在等候衔接的线程,假如存在则将衔接加入到公正行列中,由行列中有等候衔接的线程则会从堵塞行列中去获取运用;假如当时没有等候衔接的线程,而且ThreadLocal中的衔接小于50,则将衔接增加到本地线程变量ThreadLocal缓存中,当时线程下次获取衔接时直接可从ThreadLocal中获取到。

总结

这次源码探求,真的感觉看到了无数个小细节,无数个小优化,积少成多。平常开发过程中,一些小的细节也一定要“扣”。