简介
HikariCP 是什么
HikariCP是一个“零开销”的 JDBC 连接池。快速、简单、可靠 是它的特性
为什么使用
- HikariCP 是目前最快的连接池。
- SpringBoot 也把它设置为默认连接池。
- HikariCP 非常轻量。笔者项目用到的 4.0.3 版本的 jar 包仅仅只有 156 KB,它的源码真的非常精炼。
使用
-- todo
源码分析
思考
- 为什么它这么快
数据库连接池已经发展了很久了,也算是比较成熟的技术,使用比较广泛的类库有 DBCP、C3P0、Druid 等等。眼看着数据库连接池已经发展到了瓶颈,所谓的性能提升也仅仅是一些代码细节的优化,HikariCP 出现后并快速地火了起来,与其他连接池相比,它的快不是普通的快,而是跨越性的快。
首先是模型上的变化,要知道对连接池的操作不外乎四个:borrow、requite、add、remove。
之前的数据库都是 “实做模型”,从抽象层面讲,它非常符合我们的现实生活,例如,某人借走我的钱,钱就不在我的钱包里了。我们熟知的 DBCP、C3P0、Druid 等等都是这样。
但是 HikariCP 就不一样了,在“实做模型”中,borrow、return、add、remove 四个动作都需要加同一把锁,即同一时刻只允许一个线程操作池,并发高时线程切换将非常频繁。因为多个线程操作同一个池塘,连接出入池需要加锁来保证线程安全。
HikariCP 是这样做的,borrow 的连接不会从池塘里取出,而是打上“已借出”的标记,return 的时候,再把这个连接的“已借出”标记去掉。可以把这种做法称为“标记模型”。“标记模型”可以实现 borrow 和 return 动作不加锁。具体怎么做到的呢?
borrow (借出)
先看 borrow 时的操作:
- 先看 ThreaLocal 中是否已经获取过,如果有直接返回(已经获取过连接的线程,归还的时候 会放入 本地缓存(ThreadLocal))
- 看池塘里哪一个连接可以借出。这里就涉及到读连接池的操作,因为池塘里的连接数量不是一成不变的,为了一致性,就必须加锁。但是,HikariCP 没有加,为什么呢?因为 HikariCP 容忍了读的不一致。borrow 的时候,我际上读的不是真正的池塘,而是当前池塘的一份快照。 HikariCP 存放连接的地方,是一个 CopyOnWriteArrayList 对象,我们知道,CopyOnWriteArrayList 是一个写时复制(写安全、读不安全)集合。
- 如何判断是否可以借出,利用 CAS ,如果可以从 未使用 状态标记为 使用中 ,则表示可以借出。
- 如果执行前三步还是没有获取到连接,则会 自旋 一段时间,看是否能够获取到新的连接(如果等待数量过多会判断是否需要创建新的,并放入 handoffQueue )。
requite (归还)
- 首先将 连接设置为 未使用
- 看目前等待获取连接的数量有多少,如果大于0,则放入 handoffQueue 供别的使用
- 如果小于 0 则放入本地缓存
add (新增)
池中不存在的
- 首先看连接池是不是被关闭
- 连接池处于正常状态,将新的连接放入池子
remove (移除)
- 查看连接是否处于可被移除的状态
- 从池子和本地缓存中移除
核心类介绍
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 还会开启一些 监控任务
- 心跳机制
- 指标监控
HikariDataSource
位置:com.zaxxer.hikari.HikariDataSource
HikariDataSource 实现 javax.sql.DataSource 。配置装配,供用户获取连接等。
HikariDataSource 持有两个 HikariPool 。为什么要这样做呢?
首先,从性能方面考虑,使用 fastPathPool 来创建连接会比 pool 更好一些,因为 pool 被 volatile 修饰了,为了保证可见性不能使用缓存。那为什么还要用到 pool 呢?
排查了一下,发现 fastPathPool 其实只有在 getConnection 时才会使用。在获取监控指标等信息的时候还是 pool 。
个人认为有以下原因:
- 被 volatile 修饰的字段在访问时会加 读写 屏障。
- fastPathPool 被 final 修饰,不会被篡改。
- 收集指标的时候 还是使用 Pool 更准确。
- 由于延迟初始化检查, getConnection 会降低。
持有 HikariPool 的两种情况。
情况一:fastPathPool = pool = new HikariPool(this)。使用有参构造new HikariDataSource(HikariConfig configuration)来创建HikariDataSource;
情况二:fastPathPool = null;pool = new HikariPool(this)。使用无参构造new HikariDataSource()来创建 HikariDataSource 。
当然,更推荐使用 情况一 ,可以更加定制化配置信息。
PropertyElf
位置: com.zaxxer.hikari.util.PropertyElf
PropertyElf 主要是 设置 Hikari 的配置,主要方法 setTargetFromProperties 。
附录
- 使用 javassist 动态生成一部分类。
在项目中直接使用 Hikari 会发现它的 jar 包中 HikariProxyCallableStatement 、HikariProxyConnection 、HikariProxyDatabaseMetaData 等等 Hikari 开头的类,但是下载的源码当中却没有。 并且在 com.zaxxer.hikari.pool.ProxyFactory 中提供的方法 全部是直接抛出异常。方法的注释中提示查看 com.zaxxer.hikari.util.JavassistProxyFactory 。
查看 JavassistProxyFactory 后发现 它使用 javassist 将 ProxyFactory 中的方法进行了替换。并且继承了 com.zaxxer.hikari.pool 包下 Proxy* 的一些类,动态的生成 Hikari 开头的类。
利用到了 maven 插件。
暂时还不明白这么做的原因是什么,等到以后再补充。
- 连接池状态下,一个复杂的 sql 所需的时间短?还是拆分成一个一个简单的 sql 在程序中做逻辑处理 速度快?
个人感觉是 拆分开 会速度快一点,毕竟 连接的开销 在项目启动的时候已经完成,使用的时候多次发送sql 并不会影响速度。反而是一个复杂的sql 可能会导致 mysql 宕机。
此说法缺乏有效数据验证,待验证后补充