HiKariCP的由来

HiKari来源是一个日语词汇,也就是“光” 的意思。创作者的意图可见一斑,也就是说希望数据库连接池能像光一样快速。事实上也正是如此,HiKariCP 号称是业界跑得最快的数据库连接池,SpringBoot 2.0也将HiKariCP作为默认的数据库连接池,其重要性也就毋庸置疑了吧~本文主要总结了一下,数据库连接池,和为什么HiKariCP有着这么优秀的性能

数据库连接池是做什么的

实际工作中,我们总会难免和数据库打交道;只要和数据库打交道,就免不了使用数据库连接池。业界知名的数据库连接池有不少,例如 c3p0、DBCP、Tomcat JDBC Connection Pool、Druid HiKariCP等。
那么什么是数据库连接池呢?
本质上就是一个池化资源,可以类比线程池,对象池等,作用都是避免重量级资源的频繁创建和销毁
那么执行SQL语句流程如下

  • 需要执行SQL时,不是直接创建,而是从池子中取一个连接
  • SQL执行完,不是关掉连接,而是将其归还到连接池中

原生的使用数据库连接池

平常工作时,我们都是通过Spring和各种持久化框架来完成数据库的操作,为了方便我们的操作,持久化框架屏蔽了很多细节,我们基本做好配置之后,就不会和数据库连接池进行交互了。
那么如何原生的使用数据库连接池呢?类似JDBC一样,也有一个流程

  1. 通过数据源获取一个数据库连接;、
  2. 创建 Statement;
  3. 执行 SQL;
  4. 通过 ResultSet 获取 SQL 执行结果;
  5. 释放 ResultSet;
  6. 释放 Statement;
  7. 释放数据库连接。
// 数据库连接池配置
HikariConfig config = new HikariConfig();
config.setMinimumIdle(1);
config.setMaximumPoolSize(2);
config.setConnectionTestQuery("SELECT 1");
config.setDataSourceClassName("org.h2.jdbcx.JdbcDataSource");
config.addDataSourceProperty("url", "jdbc:h2:mem:test");
// 创建数据源
DataSource ds = new HikariDataSource(config);
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
  // 获取数据库连接
  conn = ds.getConnection();
  // 创建 Statement 
  stmt = conn.createStatement();
  // 执行 SQL
  rs = stmt.executeQuery("select * from abc");
  // 获取结果
  while (rs.next()) {
    int id = rs.getInt(1);
    ......
  }
} catch(Exception e) {
   e.printStackTrace();
} finally {
  // 关闭 ResultSet
  close(rs);
  // 关闭 Statement 
  close(stmt);
  // 关闭 Connection
  close(conn);
}
// 关闭资源
void close(AutoCloseable rs) {
  if (rs != null) {
    try {
      rs.close();
    } catch (SQLException e) {
      e.printStackTrace();
    }
  }
}

HiKariCP高性能的原理

首先HiKariCP作者对数据库连接池各种可以优化的地方都做了优化。比如从字节码的角度优化
而宏观上主要是和两个数据结构有关,一个是 FastList,另一个是 ConcurrentBag

FastList

数据库连接池中List的作用
按照规范步骤,执行完数据库操作之后,需要依次关闭 ResultSet、Statement、Connection,。为了解决忘记关闭问题,最好的办法是当关闭 Connection 时,能够自动关闭 Statement。为了达到这个目标,Connection 就需要跟踪创建的 Statement,最简单的办法就是将创建的 Statement 保存在数组 ArrayList 里,这样当关闭 Connection 的时候,就可以依次将数组中的所有 Statement 关闭。

FastList也就是快速列表,主要优化的是ArrayList的顺序读取
当关闭 Statement 的时候,需要调用 ArrayList 的 remove() 方法来将其从 ArrayList 中删除,这里是有优化余地的。因为删除的话ArrayList每次都是顺序遍历,要遍历一遍数组。
HiKariCP 中的 FastList 相对于 ArrayList 的一个优化点就是将 remove(Object element) 方法的查找顺序变成了逆序查找。除此之外,FastList 还有另一个优化点,get(int index) 方法没有对 index 参数进行越界检查HiKariCP 能保证不会越界,所以不用每次都进行越界检查。

ConcurrentBag

实现一个数据库连接池,最简单的办法就是用两个阻塞队列来实现,一个用于保存空闲数据库连接的队列 idle,另一个用于保存忙碌数据库连接的队列 busy

// 忙碌队列
BlockingQueue<Connection> busy;
// 空闲队列
BlockingQueue<Connection> idle;
//使用JDK阻塞队列最大的问题就是锁等待

HiKariCP 并没有使用 Java SDK 中的阻塞队列,而是自己实现了一个叫做 ConcurrentBag 的并发容器
ConcurrentBag 中最关键的属性有 4 个,分别是:用于存储所有的数据库连接的共享队列 sharedList、线程本地存储 threadList、等待数据库连接的线程数 waiters 以及分配数据库连接的工具 handoffQueue。其中,handoffQueue 用的是 Java SDK 提供的 SynchronousQueue,SynchronousQueue 主要用于线程之间传递数据。

// 用于存储所有的数据库连接
CopyOnWriteArrayList<T> sharedList;
// 线程本地存储中的数据库连接
ThreadLocal<List<Object>> threadList;
// 等待数据库连接的线程数
AtomicInteger waiters;
// 分配数据库连接的工具
SynchronousQueue<T> handoffQueue;

通过 ConcurrentBag 提供的 borrow() 方法,可以获取一个空闲的数据库连接,borrow() 的主要逻辑是:

  1. 首先查看线程本地存储是否有空闲连接,如果有,则返回一个空闲的连接;
  2. 如果线程本地存储中无空闲连接,则从共享队列中获取。
  3. 如果共享队列中也没有空闲的连接,则请求线程需要等待。

需要注意的是,线程本地存储中的连接是可以被其他线程窃取的,所以需要用 CAS 方法防止重复分配。在共享队列中获取空闲连接,也采用了 CAS 方法防止重复分配。

总结

FastList 适用于逆序删除场景;
ConcurrentBag 通过 ThreadLocal 做一次预分配,避免直接竞争共享资源,非常适合池化资源的分配。