* 戳上方蓝字“后端技术互联”关注我

大家好,我是技术Z先生,一名从事后端技术十余年且热爱分享的程序员!

今天要分享的是mysql数据库连接HikariCP为何比Druid快!

引言

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 的体积,非常轻巧。"

这段简介真是简洁而全面。再配上下面这张图。

看到这些数据,再加上 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 的性能更高。因此,建议使用有参构造方法创建 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;

回到 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();     }  }

首先,从 ThreadLocal 中获取先前使用过的连接。ThreadLocal 是当前线程的缓存,加快了本地连接获取的速度。

如果 ThreadLocal 中没有获取到连接,会尝试从 sharedList 中获取。sharedList 是 CopyOnWriteArrayList 类型的,支持写时复制,非常适合读多写少的场景。

如果在 sharedList 中也没有获取到连接,就会进入阻塞队列等待,看是否有连接被归还可供使用。

释放连接

当连接使用完毕后,我们需要释放它,通过 connection.close() 方法释放连接,而 HikariCP 也在这里做了一些巧妙的优化。ProxyConnection 的 close() 方法是 HikariCP 释放连接的实现逻辑。我们知道在关闭连接之前必须先关闭 Statement,HikariCP 在这里进行了优化,让我们来看一下具体实现。
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();     }  }
使用 FastList 存储 Statement 对象,这也是 HikariCP 之所以快的原因之一。

为什么使用 FastList 而不是 ArrayList 呢?

FastList 去掉了范围检查:查看源码会发现,FastList 的 get() 方法比 ArrayList 少了一行代码 rangeCheck(index),这行代码用于范围检查,去掉这行代码必然会提高性能。这个细节真是令人惊叹,无处不在。

FastList 的删除操作从尾部开始:FastList 的 remove() 方法是从尾部开始扫描的,而不是从头部开始扫描的。这是因为 Connection 的打开和关闭顺序通常是相反的。FastList 的根据下标删除方法也去掉了索引范围检查。

关闭了 Statement 之后,我们回头继续往下看。

poolEntry.recycle(lastAccess);

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 中获取。

总结

这次源码探究,就像是打开了一扇奇妙的大门,透过它我看到了无数个微小的宝藏,每一个都是一个小细节的优化,而这些小优化积少成多,为整个系统带来了巨大的提升。

——The  End——

今天的分享就到这里了,喜欢的小伙伴请在下方免费点赞+在看

关注我,每天学点后端技术,带你技术充点电!

对AI和后端技术感兴趣的同学可以关注以下几个公众号

圈友互联:免费使用chatgpt4.0及midjourney,AI相关工具交流分享
AI资讯互联:天分享最新科技资讯,指导大家如何使用AI工具完成实操变现
开源互联:每天开源一套优质项目,0编程基础人员也可直接下载部署
推荐阅读
畅游互联网详细指南“附下载及使用”
Midjourney V6 + ChatGPT 4 王炸组合:让你完成惊艳全网图片创作
重磅干货分享!注册google、tiktok一网打尽

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。