简介
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 )。
// 连接池中保存 连接 的集合
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 (归还)
- 首先将 连接设置为 未使用
- 看目前等待获取连接的数量有多少,如果大于0,则放入 handoffQueue 供别的使用
- 如果小于 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 (新增)
池中不存在的
- 首先看连接池是不是被关闭
- 连接池处于正常状态,将新的连接放入池子
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 (移除)
- 查看连接是否处于可被移除的状态
- 从池子和本地缓存中移除
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 还会开启一些 监控任务
- 心跳机制
- 指标监控
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 。
个人认为有以下原因:
- 被 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 。
当然,更推荐使用 情况一 ,可以更加定制化配置信息。
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 。
附录
- 使用 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 插件。
<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>
暂时还不明白这么做的原因是什么,等到以后再补充。
- 连接池状态下,一个复杂的 sql 所需的时间短?还是拆分成一个一个简单的 sql 在程序中做逻辑处理 速度快?
个人感觉是 拆分开 会速度快一点,毕竟 连接的开销 在项目启动的时候已经完成,使用的时候多次发送sql 并不会影响速度。反而是一个复杂的sql 可能会导致 mysql 宕机。
此说法缺乏有效数据验证,待验证后补充