HikariCP 源码详解

doMore 991 2023-07-24

简介

HikariCP 是什么

HikariCP是一个“零开销”的 JDBC 连接池。快速、简单、可靠 是它的特性

为什么使用

  1. HikariCP 是目前最快的连接池。
  2. SpringBoot 也把它设置为默认连接池。
  3. HikariCP 非常轻量。笔者项目用到的 4.0.3 版本的 jar 包仅仅只有 156 KB,它的源码真的非常精炼。

使用

-- todo

源码分析

思考

  1. 为什么它这么快

数据库连接池已经发展了很久了,也算是比较成熟的技术,使用比较广泛的类库有 DBCP、C3P0、Druid 等等。眼看着数据库连接池已经发展到了瓶颈,所谓的性能提升也仅仅是一些代码细节的优化,HikariCP 出现后并快速地火了起来,与其他连接池相比,它的快不是普通的快,而是跨越性的快。

首先是模型上的变化,要知道对连接池的操作不外乎四个:borrow、requite、add、remove。
之前的数据库都是 “实做模型”,从抽象层面讲,它非常符合我们的现实生活,例如,某人借走我的钱,钱就不在我的钱包里了。我们熟知的 DBCP、C3P0、Druid 等等都是这样。
但是 HikariCP 就不一样了,在“实做模型”中,borrow、return、add、remove 四个动作都需要加同一把锁,即同一时刻只允许一个线程操作池,并发高时线程切换将非常频繁。因为多个线程操作同一个池塘,连接出入池需要加锁来保证线程安全。

HikariCP 是这样做的,borrow 的连接不会从池塘里取出,而是打上“已借出”的标记,return 的时候,再把这个连接的“已借出”标记去掉。可以把这种做法称为“标记模型”。“标记模型”可以实现 borrow 和 return 动作不加锁。具体怎么做到的呢?

borrow (借出)

先看 borrow 时的操作:

  1. 先看 ThreaLocal 中是否已经获取过,如果有直接返回(已经获取过连接的线程,归还的时候 会放入 本地缓存(ThreadLocal))
  2. 看池塘里哪一个连接可以借出。这里就涉及到读连接池的操作,因为池塘里的连接数量不是一成不变的,为了一致性,就必须加锁。但是,HikariCP 没有加,为什么呢?因为 HikariCP 容忍了读的不一致。borrow 的时候,我际上读的不是真正的池塘,而是当前池塘的一份快照。 HikariCP 存放连接的地方,是一个 CopyOnWriteArrayList 对象,我们知道,CopyOnWriteArrayList 是一个写时复制(写安全、读不安全)集合。
  3. 如何判断是否可以借出,利用 CAS ,如果可以从 未使用 状态标记为 使用中 ,则表示可以借出。
  4. 如果执行前三步还是没有获取到连接,则会 自旋 一段时间,看是否能够获取到新的连接(如果等待数量过多会判断是否需要创建新的,并放入 handoffQueue )。
// 连接池中保存 连接 的集合
private final CopyOnWriteArrayList<T> sharedList;
public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException
{
// Try the thread-local list first
final var list = threadList.get();
for (int i = list.size() - 1; i >= 0; i--) {
final var 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;
}
}
// Otherwise, scan the shared list ... then poll the handoff queue
final int waiting = waiters.incrementAndGet();
try {
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;
}
}
listener.addBagItem(waiting);
timeout = timeUnit.toNanos(timeout);
do {
final var 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();
}
}

requite (归还)

  1. 首先将 连接设置为 未使用
  2. 看目前等待获取连接的数量有多少,如果大于0,则放入 handoffQueue 供别的使用
  3. 如果小于 0 则放入本地缓存
public void requite(final T bagEntry)
{
bagEntry.setState(STATE_NOT_IN_USE);
for (var 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 var threadLocalList = threadList.get();
if (threadLocalList.size() < 50) {
threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry);
}
}

add (新增)

池中不存在的

  1. 首先看连接池是不是被关闭
  2. 连接池处于正常状态,将新的连接放入池子
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();
}
}

remove (移除)

  1. 查看连接是否处于可被移除的状态
  2. 从池子和本地缓存中移除
public boolean remove(final T bagEntry)
{
if (!bagEntry.compareAndSet(STATE_IN_USE, STATE_REMOVED) && !bagEntry.compareAndSet(STATE_RESERVED, STATE_REMOVED) && !closed) {
LOGGER.warn("Attempt to remove an object from the bag that was not borrowed or reserved: {}", bagEntry);
return false;
}
final boolean removed = sharedList.remove(bagEntry);
if (!removed && !closed) {
LOGGER.warn("Attempt to remove an object from the bag that does not exist: {}", bagEntry);
}
threadList.get().remove(bagEntry);
return removed;
}

核心类介绍

ConcurrentBag

位置: com.zaxxer.hikari.util.ConcurrentBag

ConcurrentBag 可以算是 HikariCP 最核心的一个类,它是 HikariCP 底层真正的连接池,上述思考中的操作 就是靠它来实现的。

属性描述
CopyOnWriteArrayList sharedList存放着状态为使用中、未使用和保留三种状态的资源对象
ThreadLocal threadList存放着当前线程归还的资源对象
SynchronousQueue handoffQueue这是一个无容量的阻塞队列,出队和入队都可以选择是否阻塞
AtomicInteger waiters当前等待获取元素的线程数

HikariPool

位置:com.zaxxer.hikari.pool.HikariPool

HikariPool 是用来管理连接池的。

属性类型和属性名说明
DataSource dataSource用于获取原生连接对象的数据源。一般我们不指定的话,使用的是DriverDataSource
ThreadPoolExecutor addConnectionExecutor执行创建连接任务的线程池。只开启一个线程执行任务。
ThreadPoolExecutor closeConnectionExecutor执行关闭原生连接任务的线程池。只开启一个线程执行任务。
ScheduledExecutorService houseKeepingExecutorService用于执行检查 idleTimeout、leakDetectionThreshold、keepaliveTime、maxLifetime 等任务的线程池。
ProxyLeakTaskFactory leakTaskFactory用于检测连接是否泄露(即:不受连接池管理的连接)。默认是空实现,可设置一个 阈值(毫秒时间)。会打印一个警告信息
ScheduledFuture<?> houseKeeperTask用于保持最小连接数,以及清除一些不活跃的连接

注意:HikariPool 还会开启一些 监控任务

  1. 心跳机制
  2. 指标监控

HikariDataSource

位置:com.zaxxer.hikari.HikariDataSource

HikariDataSource 实现 javax.sql.DataSource 。配置装配,供用户获取连接等。

private final HikariPool fastPathPool;
private volatile HikariPool pool;

HikariDataSource 持有两个 HikariPool 。为什么要这样做呢?

首先,从性能方面考虑,使用 fastPathPool 来创建连接会比 pool 更好一些,因为 pool 被 volatile 修饰了,为了保证可见性不能使用缓存。那为什么还要用到 pool 呢?

排查了一下,发现 fastPathPool 其实只有在 getConnection 时才会使用。在获取监控指标等信息的时候还是 pool 。

个人认为有以下原因:

  1. 被 volatile 修饰的字段在访问时会加 读写 屏障。
  2. fastPathPool 被 final 修饰,不会被篡改。
  3. 收集指标的时候 还是使用 Pool 更准确。
  4. 由于延迟初始化检查, getConnection 会降低。

持有 HikariPool 的两种情况。

情况一:fastPathPool = pool = new HikariPool(this)。使用有参构造new HikariDataSource(HikariConfig configuration)来创建HikariDataSource;

情况二:fastPathPool = null;pool = new HikariPool(this)。使用无参构造new HikariDataSource()来创建 HikariDataSource 。

当然,更推荐使用 情况一 ,可以更加定制化配置信息。

public Connection getConnection() throws SQLException
{
if (fastPathPool != null) {
return fastPathPool.getConnection();
}
// 双重检查 pool 的初始化
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());
}
}
}
return result.getConnection();
}

PropertyElf

位置: com.zaxxer.hikari.util.PropertyElf

PropertyElf 主要是 设置 Hikari 的配置,主要方法 setTargetFromProperties

附录

  1. 使用 javassist 动态生成一部分类。

在项目中直接使用 Hikari 会发现它的 jar 包中 HikariProxyCallableStatementHikariProxyConnectionHikariProxyDatabaseMetaData 等等 Hikari 开头的类,但是下载的源码当中却没有。 并且在 com.zaxxer.hikari.pool.ProxyFactory 中提供的方法 全部是直接抛出异常。方法的注释中提示查看 com.zaxxer.hikari.util.JavassistProxyFactory 。

查看 JavassistProxyFactory 后发现 它使用 javassist 将 ProxyFactory 中的方法进行了替换。并且继承了 com.zaxxer.hikari.pool 包下 Proxy* 的一些类,动态的生成 Hikari 开头的类。

利用到了 maven 插件。

<plugin>
<!-- Generate proxies -->
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.6.0</version>
<extensions>true</extensions>
<executions>
<execution>
<phase>compile</phase>
<!-- phase>generate-test-sources</phase -->
<goals>
<goal>exec</goal>
</goals>
</execution>
</executions>
<configuration>
<executable>java</executable>
<arguments>
<argument>-cp</argument>
<argument>${project.build.outputDirectory}${path.separator}${maven.compile.classpath}</argument>
<!-- 会执行 JavassistProxyFactory 类的 main 方法 -->
<argument>com.zaxxer.hikari.util.JavassistProxyFactory</argument>
<argument>${project.basedir}${file.separator}</argument>
</arguments>
</configuration>
</plugin>

暂时还不明白这么做的原因是什么,等到以后再补充。

  1. 连接池状态下,一个复杂的 sql 所需的时间短?还是拆分成一个一个简单的 sql 在程序中做逻辑处理 速度快?

个人感觉是 拆分开 会速度快一点,毕竟 连接的开销 在项目启动的时候已经完成,使用的时候多次发送sql 并不会影响速度。反而是一个复杂的sql 可能会导致 mysql 宕机。
此说法缺乏有效数据验证,待验证后补充