# 如果启动连接池时不能成功初始化连接,是否快速失败 TODO
# >0 时,会尝试获取连接。如果获取时间超过指定时长,不会开启连接池,并抛出异常
# =0 时,会尝试获取并验证连接。如果获取成功但验证失败则不开启池,但是如果获取失败还是会开启池
# <0 时,不管是否获取或校验成功都会开启池。
# 默认为 1
initializationFailTimeout=1
# 是否在事务中隔离 HikariCP 自己的查询。
# autoCommit 为 false 时才生效
# 默认 false
isolateInternalQueries=false
# 是否允许通过 JMX 挂起和恢复连接池
# 默认为 false
allowPoolSuspension=false
# 当连接从池中取出时是否设置为只读
# 默认值 false
readOnly=false
# 是否开启 JMX
# 默认 false
registerMbeans=true
# 数据库 catalog
# 默认由驱动决定
# catalog=
# 在每个连接创建后、放入池前,需要执行的初始化语句
# 如果执行失败,该连接会被丢弃
# 默认为空
# connectionInitSql=
# JDBC 驱动使用的 Driver 实现类
# 一般根据 jdbcUrl 判断就行,报错说找不到驱动时才需要加
# 默认为空
# driverClassName=
# 连接的默认事务隔离级别
# 默认值为空,由驱动决定
# transactionIsolation=
# 校验连接活性允许的超时时间
# 默认 5000 ms,最小值为 250 ms,要求小于 connectionTimeout。支持 JMX 动态修改
validationTimeout=5000
# 连接对象可以被借出多久
# 默认 0(不开启),最小允许值为 2000 ms。支持 JMX 动态修改
leakDetectionThreshold=0
# 直接指定 DataSource 实例,而不是通过 dataSourceClassName 来反射构造
# 默认为空,只能通过代码设置
# dataSource=
# 数据库 schema
# 默认由驱动决定
# schema=
# 指定连接池获取线程的 ThreadFactory 实例
# 默认为空,只能通过代码设置
# threadFactory=
# 指定连接池开启定时任务的 ScheduledExecutorService 实例(建议设置setRemoveOnCancelPolicy(true))
# 默认为空,只能通过代码设置
# scheduledExecutor=
# JNDI 配置的数据源名
# 默认为空
# dataSourceJndiName=
HikariCP 的源码少且精,可读性非常高。如果你没见过像诗一样的代码,可以来看看 HikariCP。
提醒一下,在阅读 HiakriCP 源码之前,需要掌握CopyOnWriteArrayList
、AtomicInteger
、SynchronousQueue
、Semaphore
、AtomicIntegerFieldUpdater
、LockSupport
等 JDK 自带类的使用。
注意,考虑到篇幅和可读性,以下代码经过删减。
HikariCP为什么快?
数据库连接池已经发展了很久了,也算是比较成熟的技术,使用比较广泛的类库有 boneCP、DBCP、C3P0、Druid 等等。眼看着数据库连接池已经发展到了瓶颈,所谓的性能提升也仅仅是一些代码细节的优化,这个时候,HikariCP 出现并快速地火了起来,与其他连接池相比,它的快不是普通的快,而是跨越性的快。下面是 JMH 测试的结果([测试项目地址](brettwooldridge/HikariCP-benchmark: JHM benchmarks for JDBC Connection Pools (github.com)))。
HikariCP 为什么快?我看网上有很多的解释,例如,大量使用 JDK 并发包的工具来避免粗颗粒度的锁、FastList 等自定义类的使用、动态代理类等等。我觉得,这些都不是主要原因。
HikariCP 之所以快,更多的还是由于抽象层面的优化。
传统模型--中规中矩的模型
连接池,顾名思义,就是一个存放连接对象的池塘。几乎所有的连接池都会从代码层面抽象出一个池塘。池里的连接数量不是一成不变的,例如,连接失效了需要移除、新连接创建、用户借出或归还连接,等等,总结起来,对连接池的操作不外乎四个:borrow、return、add、remove。
连接池一般是这样设计的:borrow、remove 动作会将连接从池塘里拿出来,add、return 动作则会往池塘里添加连接。我把这种模型称为“传统模型”。
“传统模型”是比较中规中矩的模型,从抽象层面讲,它非常符合我们的现实生活,例如,某人借走我的钱,钱就不在我的钱包里了。我们熟知的 DBCP、C3P0、Druid 等等都是基于“传统模型”开发的。
标记模型--更少的锁
但是 HikariCP 就不一样了,它没有走老路,而是优化了“传统模型”,让连接池真正意义地实现了提速。
在“传统模型”中,borrow、return、add、remove 四个动作都需要加同一把锁,即同一时刻只允许一个线程操作池,并发高时线程切换将非常频繁。因为多个线程操作同一个池塘,连接出入池需要加锁来保证线程安全。针对这一点,我们是不是能做些什么呢?
HikariCP 是这样做的,borrow 的连接不会从池塘里取出,而是打上“已借出”的标记,return 的时候,再把这个连接的“已借出”标记去掉。我把这种做法称为“标记模型”。“标记模型”可以实现 borrow 和 return 动作不加锁。具体怎么做到的呢?
首先,我要 borrow 时,我需要看看池塘里哪一个连接可以借出。这里就涉及到读连接池的操作,因为池塘里的连接数量不是一成不变的,为了一致性,我们就必须加锁。但是,HikariCP 没有加,为什么呢?因为 HikariCP 容忍了读的不一致。borrow 的时候,我们实际上读的不是真正的池塘,而是当前池塘的一份快照。我们看看 HikariCP 存放连接的地方,是一个CopyOnWriteArrayList
对象,我们知道,CopyOnWriteArrayList
是一个写安全、读不安全的集合。
public class ConcurrentBag<T extends IConcurrentBagEntry> implements AutoCloseable {
// 存放连接的集合
private final CopyOnWriteArrayList<T> sharedList;
接着,当我们找到了一个可借出的连接时,需要给它打上借出的标记。注意,这时有可能出现多个线程都想给它打标记的情况,该怎么办呢?难道要加锁了吗?别忘了我们可以用 CAS 机制来更新连接的标记,这个时候就不需要加锁了。看看 HikariCP 就是这么实现的。
public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException {
final int waiting = waiters.incrementAndGet();
try {
for (T bagEntry : sharedList) {
if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
return bagEntry;
return null;
finally {
waiters.decrementAndGet();
于是,在“标记模型”里,只有 add 和 remove 才需要加锁,borrow 和 return 不需要加锁。通过这种颠覆式的设计,连接池的性能得到极大的提高。
这就是我认为的 HikariCP 快的最主要原因。
HikariCP主要的类
那么,我们来看看 HikariCP 的源码吧。
HikariCP 的类不多,最主要的就这几个,如图。不难发现,这种类结构和 DBCP2 很像。
这几个类可以分成四个部分:
用户接口。用户一般会使用DataSource.getConnection()
来获取连接对象。
JMX 支持。
配置信息。使用HikariConfig
加载配置文件,或手动配置HikariConfig
的参数,它一般会作为入参来构造HikariDataSource
对象;
连接池。获取连接的过程为HikariDataSource.getConnection()
->HikariPool.getConnection()
->ConcurrentBag.borrow(long, TimeUnit)
。需要注意的是,ConcurrentBag
才是真正的连接池,而HikariPool
是用来管理连接池的。
ConcurrentBag--最核心的类
ConcurrentBag
可以算是 HikariCP 最核心的一个类,它是 HikariCP 底层真正的连接池,上面说的“标记模型”只要就是靠它来实现的。如果大家不想看太多代码的话,只看它就足够了。
在设计上,ConcurrentBag
是一个比较通用的资源池,它可以是数据库连接的池,也可以是其他对象的池,只要存放的资源对象实现了IConcurrentBagEntry
接口即可。所以,如果我们的项目中需要自己构建池的话,可以直接拿这个现成的组件来用。
public class ConcurrentBag<T extends IConcurrentBagEntry> implements AutoCloseable {
private final CopyOnWriteArrayList<T> sharedList;
下面简单介绍下ConcurrentBag
的几个字段:
这几个字段在ConcurrentBag
中如何使用呢,这里拿borrow
方法来说明下:
public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException
// 1. 首先从threadList获取资源
final List<Object> list = threadList.get();
for (int i = list.size() - 1; i >= 0; i--) {
final Object entry = list.remove(i);
final T bagEntry = weakThreadLocals ? ((WeakReference<T>) entry).get() : (T) entry;
if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
return bagEntry;
// 等待获取连接的线程数+1
final int waiting = waiters.incrementAndGet();
try {
// 2.如果还没获取到,会从sharedList中获取对象
for (T bagEntry : sharedList) {
if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
// 这一步我不是很懂,好像可有可无
if (waiting > 1) {
listener.addBagItem(waiting - 1);
return bagEntry;
// 从sharedList中获取不到资源,通知监听器创建资源(不一定会创建)
listener.addBagItem(waiting);
// 3.如果还没获取到,会堵塞等待空闲连接
timeout = timeUnit.toNanos(timeout);
final long start = currentTime();
// 这里会出现三种情况,
// 1.超时,返回null
// 2.获取到资源,但状态为正在使用,继续循环
// 3.获取到资源,元素状态为未使用,修改为已使用并返回
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);
// 4.超时了还是没有获取到,返回null
return null;
finally {
// 等待获取连接的线程数-1
waiters.decrementAndGet();
在上面的方法中,唯一会造成线程阻塞的就是handoffQueue.poll(timeout, NANOSECONDS)
,除此之外,我们没有看到任何的 synchronized 和 lock。
HikariPool--管理连接池
除了ConcurrentBag
,HikariPool
也是一个比较重要的类,它用来管理连接池。
HikariPool 的几个字段说明如下:
属性类型和属性名
为了更清晰地理解上面几个字段的含义,我简单画了个图,不是很严谨,将就看下吧。在这个图中,客户端线程可以调用进行 borrow、requite 和 remove 操作,houseKeepingExecutorService 线程可以调用进行 remove 操作,只有 addConnectionExecutor 可以进行 add 操作。
一些有趣的地方
掌握了上面的两个类,HikariCP 的整个源码视图应该就比较完整了。下面再说一些有趣的地方。
为什么HikariDataSource有两个HikariPool
在下面的代码中,HikariDataSource
里竟然有两个HikariPool
。
public class HikariDataSource extends HikariConfig implements DataSource, Closeable
private final HikariPool fastPathPool;
private volatile HikariPool pool;
为什么要这样做呢?
首先,从性能方面考虑,使用 fastPathPool 来创建连接会比 pool 更好一些,因为 pool 被 volatile 修饰了,为了保证可见性不能使用缓存。那为什么还要用到 pool 呢?
我们打开HikariDataSource.getConnection()
,可以看到,pool 的存在可以用来支持双重检查锁。这里我比较好奇的是,为什么不把 HikariPool
的引用给 fastPathPool??这个问题大家感兴趣可以研究一下。
public Connection getConnection() throws SQLException
if (fastPathPool != null) {
return fastPathPool.getConnection();
HikariPool result = pool;
if (result == null) {
synchronized (this) {
result = pool;
if (result == null) {
validate();
pool = result = new HikariPool(this);
this.seal();
return result.getConnection();
其实,这两个HikariPool
对象有两种取值情况:
取值一:fastPathPool = pool = new HikariPool(this)
。当通过有参构造new HikariDataSource(HikariConfig configuration)
来创建HikariDataSource
就会出现这样取值;
取值二:fastPathPool = null;pool = new HikariPool(this)
。当通过无参构造new HikariDataSource()
来创建HikariDataSource
就会出现这样取值。
所以,我更推荐使用new HikariDataSource(HikariConfig configuration)
的方式,因为这样做的话,我们将使用 fastPathPool 来获取连接。
如何加载配置
HikariCP 加载配置的代码非常简洁。我们直接从PropertyElf.setTargetFromProperties(Object, Properties)
方法开始看,如下。
// 这个方法就是将properties的参数设置到HikariConfig中
public static void setTargetFromProperties(final Object target, final Properties properties)
if (target == null || properties == null) {
return;
// 获取HikariConfig的所有方法
List<Method> methods = Arrays.asList(target.getClass().getMethods());
properties.forEach((key, value) -> {
// 如果是dataSource.*的参数,直接加入到dataSourceProperties属性
if (target instanceof HikariConfig && key.toString().startsWith("dataSource.")) {
((HikariConfig) target).addDataSourceProperty(key.toString().substring("dataSource.".length()), value);
else {
// 找到参数对应的setter方法并赋值
setProperty(target, key.toString(), value, methods);
相比其他类库(尤其是 druid),HikariCP 加载配置的过程非常简洁,不需要按照参数名一个个地加载,这样后期会更好维护。当然,这种方式我们也可以运用到实际项目中。
另外,配置 HikariCP 的时候不允许写错参数或者添加一些无关的参数,否则会因为找不到对应的 setter 方法而报错。
以上基本讲完 HikariCP 的源码。后续发现其他有趣的地方再做补充,也欢迎大家指正不足的地方。
最后,感谢阅读。
HikariCP github
2021-05-20 修改
相关源码请移步:https://github.com/ZhangZiSheng001/hikari-demo
本文为原创文章,转载请附上原文出处链接: https://www.cnblogs.com/ZhangZiSheng001/p/12329937.html