专栏名称: Java知音
专注于Java,推送技术文章,热门开源项目等。致力打造一个有实用,有情怀的Java技术公众号!
目录
相关文章推荐
老乡俱乐部乡宁站  ·  选址公布!全球500强企业再与山西签约... ·  昨天  
JC万事通  ·  晋城降雪!最新消息! ·  昨天  
山西省人民政府  ·  海报|亚冬会上的“山西能量包”,Buff叠满! ·  3 天前  
51好读  ›  专栏  ›  Java知音

替代Druid,HakariCP 为什么这么快?

Java知音  · 公众号  ·  · 2024-03-24 11:30

正文

戳上方蓝字“ Java知音 ”关注我

引言

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.zaxxergroupId>  
    <artifactId>HikariCPartifactId>  
    <version>4.0.3version>  
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 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         // 向创建连接线程池中提交创建连接的任务  
        addConnectionExecutor.submit((i 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 sharedList;  
private final boolean weakThreadLocals;  
  
private final ThreadLocal> threadList;  
private final IBagStateListener listener;  
private final AtomicInteger waiters;  
private volatile boolean closed;  
  
private final SynchronousQueue handoffQueue;  

回到 borrow() 方法,看一下borrow的实现逻辑。

public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException  
{  
   // 从ThreadLocal中获取当前线程绑定的对象集合,存在则获取  
   final List 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) 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是当前线程的缓存,加快本地连接获取速度。







请到「今天看啥」查看全文