HikariCP 了解一下

article/2025/9/14 4:13:38

作者 | 黄永灿

后端开发攻城狮,关注服务端技术与性能优化。

前言

在我们的工作中,免不了要和数据库打交道,而要想和数据库打好交道,选择一款合适的数据库连接池就至关重要,目前业界广为人知的数据库连接池有 Tomcat JDBC Connection Pool、c3p0、DBCP、BoneCP、Druid 等,而我们这次要介绍的主角是 HiKariCP,HiKariCP 号称业界跑得最快的数据库连接池,近几年发展的风生水起,更是被 Spring Boot 2.0 选中作为其默认数据库连接池,基本上是坐实了江湖一哥的地位,今天咱们就来分析一下为什么它能跑得这么快。

HiKariCP 全称 HiKari Connection Pool,HiKari 源自日语 - 光,你可以这样读 hi-ka-le

有多快

以下数据摘自 HikariCP 官方,可以看到,不管是获取-关闭数据库连接还是执行语句,其速度均远高于其他产品。

什么是数据库连接池

在揭 HiKari 老底之前,我们先简单介绍(回顾)一下什么是数据库连接池。我们都知道在 Java 里面,所有的线程创建和调度都是委托给操作系统的,也就是说 Java 里面的线程是和操作系统的线程一一对应的,这样做的好处是稳定可靠,因为操作系统在这方面非常成熟,但缺点也是显而易见的,创建成本太高了,所以我们想办法弄出了各种各样的线程池,而本质上,数据库连接池和线程池一样,都属于池化资源,作用都是避免重量级资源的频繁创建和销毁,只不过对于数据库连接池来说,这个重量级资源不是线程而是数据库连接。

当我们使用数据库连接池后,在程序运行时连接池会保有一定数量的数据库连接,当需要执行 SQL 时,并不是直接创建一个数据库连接,而是从连接池中获取一个,当 SQL 执行完,再把这个数据库连接归还给连接池。

为什么这么快

为什么 HiKariCP 能跑这么快?实际上 JDBC 连接池的实现并不复杂,主要是对 JDBC 中几个核心对象 Connection、Statement、PreparedStatement、CallableStatement 以及 ResultSet 的封装与动态代理,能够优化的空间不大,HiKariCP 究竟是有什么本领能在极少的代码量(仅两千多行)上做到比其他数据库连接池快那么多呢?

1.优化字节码

HikariCP 对 java.sql.* 提供了五个代理类

  1. ProxyConnection (proxy class for java.sql.Connection)

  2. ProxyStatement (proxy class for java.sql.Statement)

  3. ProxyPreparedStatement (proxy class for java.sql.PreparedStatement)

  4. ProxyCallableStatement (proxy class for java.sql.CallableStatement)

  5. ProxyResultSet (proxy class for java.sql.ResultSet)

然后再提供了一个 ProxyFactory 来获得这几个代理类,但当我们查看 ProxyFactory 源码时会发现,其方法体里面都是直接抛异常,而没有具体实现,以 getProxyStatement 举例。

static Statement getProxyStatement(final ProxyConnection connection, final Statement statement) {// Body is replaced (injected) by JavassistProxyFactory// 方法 body 中的代码在编译时调用 JavassistProxyFactory 生成throw new IllegalStateException("You need to run the CLI build and you need target/classes in your classpath to run.");
}

HiKariCP 觉得用 JDK 代理和 CGLIB 代理还是太慢了,所以利用了一个第三方的 Java 字节码修改类库 Javassist 来生成委托实现动态代理,其生成出来的字节码更少更精简,动态代理性能大概是 CGLIB 代理的五倍,JDK 代理(jdk1.8之前)的十倍。具体性能对比可以参考这篇博客动态代理方案性能对比。

2.自定义并发容器 ConcurrentBag

在介绍 ConcurrentBag 容器前,我们先来想一下,如果让来实现一个数据库连接池,我们会采用何种数据结构?比较简单的办法就是使用两个阻塞队列 a 和 b 分别存储空闲的数据库连接和使用中的数据库连接,getConnection 时将数据库连接从 a 队列移至 b 队列,connection.close 时再将数据库连接从 b 移至 a。这种方案实现起来简单,但是性能并不是很理想,因为 Java 里面的阻塞队列是用锁实现的,而高并发场景下锁的争用对性能影响很大。

ConcurrentBag 是 HiKariCP 专门为连接池设计的一个 lock-less 集合,实现了比阻塞队列更好并发性能,它的核心思想是使用 ThreadLocal 来避免一定的并发问题,其主要结构如下。

public class ConcurrentBag<T extends IConcurrentBagEntry> implements AutoCloseable {// 用于存储所有的数据库连接private final CopyOnWriteArrayList<T> sharedList;// 线程本地存储中的数据库连接private final ThreadLocal<List<Object>> threadList;// 用于存在资源等待线程时的第一手资源交接private final SynchronousQueue<T> handoffQueue;
}

CopyOnWriteArrayList 是 juc 包里面的一个线程安全的集合,基于 Copy-On-Write 思想,读操作完全无锁,写操作时加锁复制一份数据出来修改,再替换原先的数据,在写加锁期间,并不会影响读操作,只不过读操作读的依旧是老数据。

SynchronousQueue 是一个是一个无存储空间的阻塞队列,非常适合做交换工作,经常用于生产者的线程和消费者的线程同步以传递某些信息、事件或者任务。因为是无存储空间的,所以与其他阻塞队列实现不同的是,这个阻塞 peek 方法直接返回 null,无任何其他操作,其他的方法与阻塞队列一致。这个队列的特点是,必须先调用 take 或者 poll 方法,才能使用 offer 和 put 方法,感兴趣可以去看下源码。

当数据库连接池初始化或扩容的时候,会创建一个新的数据库连接(即 T bagEntry)并调用 ConcurrentBag 的 add 方法将其添加到 sharedList 中,如果这时有线程正在等待获取数据库连接,则通过 handoffQueue 将这个连接分配给等待的线程。

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// 如果有等待连接的线程,则通过 handoffQueue 直接分配给等待的线程while (waiters.get() > 0 && bagEntry.getState() == STATE_NOT_IN_USE && !handoffQueue.offer(bagEntry)) {yield();}
}

当我们想从连接池获取一个数据库连接时,HiKari 会调用 ConcurrentBag 的 borrow 方法,borrow 方法的主要逻辑是:

  1. 首先查看线程本地存储是否有空闲连接,如果有,则返回一个空闲的连接;

  2. 如果线程本地存储中无空闲连接,则从共享队列中获取。

  3. 如果共享队列中也没有空闲的连接,则请求线程需要等待。

public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException {// Try the thread-local list first// 先查看线程本地存储是否有空闲连接final List<Object> list = threadList.get();for (int i = list.size() - 1; i >= 0; i--) {final Object entry = list.remove(i);// weakThreadLocals 是用于判断 ThreadLocal 里面存的是连接的弱引用还是强引用// 可以通过配置项 com.zaxxer.hikari.useWeakReferences 设置,但官方不推荐覆盖// 当 ConcurrentBag 的类加载器和系统的类加载器不一样时是 true,默认是 falsefinal T bagEntry = weakThreadLocals ? ((WeakReference<T>) entry).get() : (T) entry;// 线程本地存储中的连接也可以被窃取(下文会解释到),所以需要用 CAS 防止重复分配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)) {return bagEntry;}}// 共享队列中没有连接,则需要等待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);// 超时没有获取到连接,返回 nullreturn null;} finally {waiters.decrementAndGet();}
}

当我们执行完 SQL 释放数据库连接时,会调用 ConcurrentBag 的 requite 方法,该方法的逻辑很简单,首先将数据库连接状态更改为 STATENOTIN_USE,之后查看是否存在等待线程,如果有,则分配给等待线程;如果没有,则将该数据库连接保存到线程本地存储里。

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 {yield();}}// 如果没有等待的线程,则进入线程本地存储final List<Object> threadLocalList = threadList.get();if (threadLocalList.size() < 50) {threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry);}
}

这里解释一下为什么说 ThreadLocal 存储的数据库连接是可以被其他线程窃取的,因为在调用 borrow 方法时,如果当前线程自己的 ThreadLocal 没有空闲的连接,则会去 sharedList 里面去找空闲的连接,这个连接有可能已经在其他线程的 ThreadLocal 里面,然后在调用 requite 方法时,如果没有等待的线程,当前线程会把这个连接加入到自己的 ThreadLocal 里面,也就是说一个数据库连接可能被多个线程的 ThreadLocal 引用。

3.自定义数组 FastList

FastList 是 HikariCP 自己实现的用来替代 ArrayList 的一个集合,主要用在 ConcurrentBag 的 threadList 上和 Connection 存储 Statement 上,因为他们觉得 ArrayList 性能不够好,没错,就是性能不满意。FastList 主要做了两个地方的优化:

  1. get(int index) 方法去掉对 index 参数进行越界检查,因为 HikariCP 能保证不越界,只会在 for 循环里面用到

  2. remove(Object element) 方法由顺序遍历查找改为逆序遍历查找

第一个优化大家应该很容易理解,能少执行一个判断,当调用很频繁时,其效果就很明显了。第二个优化跟数据库连接池的业务有很大关系,当我们执行完 SQL 后,按照规范,需要关闭 Connection 和 Statement,而关闭 Statement 时需要将 Statement 从 Connection 以逆序的方式移除,如果按照 ArrayList 的 remove 方式,将 n 个 Statement 移除总共要 n + n-1 + ... + 1 次,而如果改为逆序,则只需要 n 次。

总结

除了以上说的几个主要优化之外,HikariCP 还做了若干细节上的优化,包括优化拦截器、对耗时超过一个 CPU 时间片的方法优化等,当然,写本文的目的也不是为了推广 HikariCP 数据库连接池,而是希望学习其中的思想,毕竟 Druid 虽然性能不如 HikariCP,但自带各种监控功能不香嘛,选择一个适合业务的才是最重要的。

参考

  1. HikariCP 官网

  2. Java并发编程实战

全文完


以下文章您可能也会感兴趣:

  • 聊聊Hystrix 命令执行流程

  • Mysql redo log 漫游

  • RabbitMQ 如何保证消息可靠性

  • 复杂业务状态的处理:从状态模式到 FSM

  • 简单聊聊 TCP 的可靠性

  • 延时队列:基于 Redis 的实现

  • 你真的懂 Builder 设计模式吗?论如何实现真正安全的 Builder 模式

  • Actor 模型及 Akka 简介

  • 从零搭建一个基于 lstio 的服务网格

  • 容器管理利器:Web Terminal 简介

我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 rd-hr@xingren.com 。


http://chatgpt.dhexx.cn/article/bH44lrgU.shtml

相关文章

离线数仓-03-数仓系统搭建(ODS,DIM,DWD,DWS,ADS)

文章目录 数仓分层为什么要分层数据运营层&#xff1a;ODS&#xff08;Operational Data Store&#xff09;数据仓库层&#xff1a;DW&#xff08;Data Warehouse&#xff09;维表层&#xff1a;DIM&#xff08;Dimension&#xff09;数据明细层&#xff1a;DWD&#xff08;Dat…

数仓数据分层(ODS DWD DWS ADS)换个角度看

数仓数据分层简介 1. 背景 数仓是什么, 其实就是存储数据,体现历史变化的一个数据仓库. 因为互联网时代到来,基于数据量的大小,分为了传统数仓和现代数仓.传统数仓,使用传统的关系型数据库进行数据存储,因为关系型数据库本身可以使用SQL以及函数等做数据分析.所以把数据存储和…

湖仓一体电商项目(十):业务实现之编写写入DWD层业务代码

文章目录 业务实现之编写写入DWD层业务代码 一、代码编写

【实时数仓】CDC简介、实现DWD层业务数据的处理(主要任务、接收kafka数据、动态分流*****)

文章目录 一 CDC简介1 什么是CDC2 CDC的种类3 Flink-CDC 二 准备业务数据-DWD层1 主要任务&#xff08;1&#xff09;接收Kafka数据&#xff0c;过滤空值数据&#xff08;2&#xff09;实现动态分流功能&#xff08;3&#xff09;把分好的流保存到对应表、主题中 2 接收Kafka数…

数仓开发之DWD层(二)

目录 三&#xff1a;流量域用户跳出事务事实表 3.1 主要任务 3.2 思路分析 3.3 图解 3.4 代码 四&#xff1a;交易域加购事务事实表 4.1 主要任务 4.2 思路分析 4.3 图解 4.4 代码 三&#xff1a;流量域用户跳出事务事实表 3.1 主要任务 过滤用户跳出明细数据。 3.2 思…

电商数仓(dwd 层)

一、dwd 层介绍 1、对用户行为数据解析。 2、对核心数据进行判空过滤。 3、对业务数据采用维度模型重新建模&#xff0c;即维度退化。 二、dwd 层用户行为数据 2.1 用户行为启动表 dwd_start_log 1、数据来源 ods_start_log -> dwd_start_log 2、表的创建 drop table…

详解数仓中的数据分层:ODS、DWD、DWM、DWS、ADS

何为数仓DW Data warehouse(可简写为DW或者DWH)数据仓库,是在数据库已经大量存在的情况下,它是一整套包括了etl、调度、建模在内的完整的理论体系。 数据仓库的方案建设的目的,是为前端查询和分析作为基础,主要应用于OLAP(on-line Analytical Processing),支持复杂的分析…

万字详解数仓分层设计架构 ODS-DWD-DWS-ADS

一、数仓建模的意义&#xff0c;为什么要对数据仓库分层&#xff1f; 只有数据模型将数据有序的组织和存储起来之后&#xff0c;大数据才能得到高性能、低成本、高效率、高质量的使用。 1、分层意义 1&#xff09;清晰数据结构&#xff1a;每一个数据分层都有它的作用域&#x…

数仓开发之DWD层(四)

目录 十一&#xff1a;工具域优惠券领取事务事实表 11.1 主要任务&#xff1a; 11.2 思路分析&#xff1a; 11.3 图解&#xff1a; 十二&#xff1a;工具域优惠券使用&#xff08;下单&#xff09;事务事实表 12.1 主要任务&#xff1a; 12.2 思路分析&#xff1a; 12.3…

数仓开发之DWD层(三)

&#xff08;附&#xff1a;由于篇幅原因&#xff0c;这里就不在展示代码了&#xff0c;直接告诉大家思路&#xff09; 目录 五&#xff1a;交易域订单预处理表 5.1 主要任务 5.2 思路分析 5.3 图解 六&#xff1a;交易域下单事务事实表 6.1 主要任务&#xff1a; 6.2 …

数仓开发之DWD层(一)

目录 一&#xff1a;流量域未经加工的事务事实表 1.1 主要任务 1.2 思路 1.3 图解 1.4 代码 二&#xff1a;流量域独立访客事务事实表 2.1 主要任务 2.2 思路分析 2.3 图解 2.4 代码 DWD层设计要点&#xff1a; &#xff08;1&#xff09;DWD层的设计依据是维度建模理论&…

数据仓库之DWD层

DWD&#xff08;Data WareHouse Detail&#xff09;数据明细层&#xff0c;主要是将从业务数据库中同步过来的ODS层数据进行清洗和整合成相应的事实表。事实表作为数据仓库维度建模的核心&#xff0c;需要紧紧围绕着业务过程来设计。在拿到业务系统的表结构后&#xff0c;进行大…

数仓建设 | ODS、DWD、DWM等理论实战(好文收藏)

本文目录&#xff1a; 一、数据流向 二、应用示例 三、何为数仓DW 四、为何要分层 五、数据分层 六、数据集市 七、问题总结 导读 数仓在建设过程中&#xff0c;对数据的组织管理上&#xff0c;不仅要根据业务进行纵向的主题域划分&#xff0c;还需要横向的数仓分层规范。本文…

数仓及其维度(分层)建模(ODS DWD DWS DWT ADS)

一. 数仓及其维度 1. 什么是数仓&#xff1f; 数据仓库&#xff0c;简称数仓,&#xff08; Data Warehouse &#xff09;。从逻辑上理解&#xff0c;数据库和数仓没有区别&#xff0c;都是通过数据库软件实现存放数据的地方&#xff0c;只不过从数据量来说&#xff0c;数据仓库…

[数据仓库]分层概念,ODS,DM,DWD,DWS,DIM的概念

目录 前言&#xff1a; 一. 各种名词解释 1.1 ODS是什么&#xff1f; 1.2 数据仓库层DW&#xff1f; 1.2.1 DWD明细层? 1.2.2 DWM 轻度汇总层(MID或DWB, data warehouse basis) 1.2.3 DWS 主题层(DM&#xff0c;data market或DWS, data warehouse service) 1.3 APP&…

详解数据仓库和数据集市:ODS、DW、DWD、DWM、DWS、ADS

一、数据流向 二、应用示例 三、何为数仓DW Data warehouse&#xff08;可简写为DW或者DWH&#xff09;数据仓库&#xff0c;是在数据库已经大量存在的情况下&#xff0c;它是一整套包括了etl、调度、建模在内的完整的理论体系。 数据仓库的方案建设的目的&#xff0c;是为前端…

数据仓库分层DWD、DWB、DWS

DW &#xff1a;data warehouse 翻译成数据仓库 DW数据分层&#xff0c;由下到上为 DWD,DWB,DWS DWD&#xff1a;data warehouse detail 细节数据层&#xff0c;有的也称为 ODS层&#xff0c;是业务层与数据仓库的隔离层 DWB&#xff1a;data warehouse base 基础数据层&#x…

数据分层详解ODS、DWD、DWM、DWS、ADS

详解数仓中的数据分层&#xff1a;ODS、DWD、DWM、DWS、ADS 何为数仓DW Data warehouse&#xff08;可简写为DW或者DWH&#xff09;数据仓库&#xff0c;是在数据库已经大量存在的情况下&#xff0c;它是一整套包括了etl、调度、建模在内的完整的理论体系。 数据仓库的方案建…

简单搞定数仓搭建:数仓模型(DWD)

明细粒度事实层&#xff08;DWD&#xff09; 明细粒度事实层以业务过程驱动建模&#xff0c;基于每个具体的业务过程特点&#xff0c;构建最细粒度的明细层事实表。您可以结合企业的数据使用特点&#xff0c;将明细事实表的某些重要维度属性字段做适当冗余&#xff0c;即宽表化…