01Hikari 源码解析之connection相关
一、初始化
类 HikariDataSource 的 getConnection() 为 取connection 的方法,主要流程如下:
- 判断 当前的 dataSource 是否关闭,如果关闭 ,抛出异常
- 判断fastPathPool 是否为null ,默认为null
- 判断当前的pool 是否为null ,如果为空,那就就行初始化,所以项目启动完了之后,第一次请求时间比较长
- 不为空 ,返回 result.getConnection()
public Connection getConnection() throws SQLException{
if (isClosed()) {
throw new SQLException("HikariDataSource " + this + " has been closed.");
}
if (fastPathPool != null) {
return fastPathPool.getConnection();
}
// See http://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java
HikariPool result = pool;
if (result == null) {
synchronized (this) {
result = pool;
... ... ...... 省略,详细过程在下面分析
xxxx .... xxxxx
}
}
}
第一次获取connection的时候,需要初始化,大致流程如下:
- 设置poolName ,前缀 HikariPool- ,后面跟的是 pool_number
- validateNumerics,这里是 对一些参数校验,具体 查看下面的表格
- 对 pool 进行初始,里面还有一系列的 参数初始化 ,这里暂不展开,主要涉及houseKeeperTask ,这个是 在delay 100ms 之后 每隔 30秒 定时运行,详细在下面概述
1.1 houseKeeperTask 定时清理 和 填充连接池
在 初始化的时候, houseKeeperTask 设置为 initialDelay = 100L, 每隔30S(默认) 运行一次
this.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, housekeepingPeriodMs, MILLISECONDS);
主要逻辑在 内部类 HouseKeeper的run() 方法里面,看一下主要的逻辑,就是不断的维护线程池里面的连接数
public void run() {
try {
... 省略 ...
previous = now;
String afterPrefix = "Pool ";
// 如果idleTimeout >0 ,默认10分钟,600000ms 并且 最小连接数 小于最大连接数
// 如果不配置 最小连接数 ,最小连接数 默认和 max_num_size 一样大,都是默认10
if (idleTimeout > 0L && config.getMinimumIdle() < config.getMaximumPoolSize()) {
logPoolState("Before cleanup ");
afterPrefix = "After cleanup ";
final List<PoolEntry> notInUse = connectionBag.values(STATE_NOT_IN_USE);
int toRemove = notInUse.size() - config.getMinimumIdle();
// 删除超时的连接
for (PoolEntry entry : notInUse) {
if (toRemove > 0 && elapsedMillis(entry.lastAccessed, now) > idleTimeout && connectionBag.reserve(entry)) {
closeConnection(entry, "(connection has passed idleTimeout)");
toRemove--;
}
}
}
logPoolState(afterPrefix);
//进行连接池补充
fillPool(); // Try to maintain minimum connections
}
catch (Exception e) {
LOGGER.error("Unexpected exception in housekeeping task", e);
}
}
private synchronized void fillPool()
{
final int connectionsToAdd = Math.min(config.getMaximumPoolSize() - getTotalConnections(), config.getMinimumIdle() - getIdleConnections())
- addConnectionQueue.size();
for (int i = 0; i < connectionsToAdd; i++) {
addConnectionExecutor.submit((i < connectionsToAdd - 1) ? POOL_ENTRY_CREATOR : POST_FILL_POOL_ENTRY_CREATOR);
}
}
1.2 创建连接 createPoolEntry
这里主要介绍一下 createPoolEntry 方法,重点就是 创建了一个 poolEntry之后,在到 maxlifetime 最大还差2.5% 时,进行标记,如果 当前空闲直接回收,如果回收成功,在判断一下是否需要新建一个poolEntry
private PoolEntry createPoolEntry()
{
try {
final PoolEntry poolEntry = newPoolEntry();
final long maxLifetime = config.getMaxLifetime();
if (maxLifetime > 0) {
// variance up to 2.5% of the maxlifetime
final long variance = maxLifetime > 10_000 ? ThreadLocalRandom.current().nextLong( maxLifetime / 40 ) : 0;
final long lifetime = maxLifetime - variance;
// 快接近最大存活时间时,对连接进行标记,如果当前连接空闲,直接回收
如果回收成功,再判断是否有等待线程,如果有说明当前资源紧张,再创建一个poolEntry
poolEntry.setFutureEol(houseKeepingExecutorService.schedule(
() -> {
if (softEvictConnection(poolEntry, "(connection has passed maxLifetime)", false /* not owner */)) {
addBagItem(connectionBag.getWaitingThreadCount());
}
},
lifetime, MILLISECONDS));
}
return poolEntry;
}
catch (Exception e) {
if (poolState == POOL_NORMAL) { // we check POOL_NORMAL to avoid a flood of messages if shutdown() is running concurrently
LOGGER.debug("{} - Cannot acquire connection from data source", poolName, (e instanceof ConnectionSetupException ? e.getCause() : e));
}
return null;
}
}
public void addBagItem(final int waiting)
{
final boolean shouldAdd = waiting - addConnectionQueue.size() >= 0; // Yes, >= is intentional.
if (shouldAdd) {
addConnectionExecutor.submit(POOL_ENTRY_CREATOR);
}
}
二、获取Connection
上面已经大致涉及第一次初始化的 过程,接下来概述一些 getConnection的流程,主要是在类HikariPool 的
getConnection() 里面,代码如下,
public Connection getConnection(final long hardTimeout) throws SQLException
{
// 获取信号量,最大值1000
suspendResumeLock.acquire();
final long startTime = currentTime();
try {
long timeout = hardTimeout;
do {
// 从connectionBag 里面 获取borrow
PoolEntry poolEntry = connectionBag.borrow(timeout, MILLISECONDS);
if (poolEntry == null) {
break; // We timed out... break and throw exception
}
final long now = currentTime();
// 判断当前的poolEntry 是否有效,无效就关闭
if (poolEntry.isMarkedEvicted() || (elapsedMillis(poolEntry.lastAccessed, now) > aliveBypassWindowMs && !isConnectionAlive(poolEntry.connection))) {
closeConnection(poolEntry, poolEntry.isMarkedEvicted() ? EVICTED_CONNECTION_MESSAGE : DEAD_CONNECTION_MESSAGE);
timeout = hardTimeout - elapsedMillis(startTime);
}
else {
metricsTracker.recordBorrowStats(poolEntry, startTime);
return poolEntry.createProxyConnection(leakTaskFactory.schedule(poolEntry), now);
}
} while (timeout > 0L); // 在connectionTimeout 时间内 一直循环直到找到可用的
metricsTracker.recordBorrowTimeoutStats(startTime);
throw createTimeoutException(startTime);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new SQLException(poolName + " - Interrupted during connection acquisition", e);
}
finally {
suspendResumeLock.release();
}
}
其流程图如下:
大致流程如下:
-
获取信号量suspendResumeLock ,这里 默认是 1000个
-
从connectionBag 里面获取 poolEntry ,这里 做了一定的优化 ,ConcurrentBag(borrow)详细的见02Hikari源码解析之ConcurrentBag、FastList分析 ,这里概述一下:
2.1. 先从 threadList 里面获取,同一个Request 即同一个线程时 从里面获取
2.2. 如果threadList 里面可用的 ,再从sharedList 里面获取 ,sharedList 存放ConcurrentBag中全部用于出借的资源 ,这里有一个注意点,这里也是 hkari 快的原因,就是判断如果 还有其他的等待者,赶紧的再去创建连接,而没有判断 当前 sharedList 包里面 是否够用了,所以 高QPS的情况下,很容易连接池达到最大的量
2.3. 当现有全部资源全部在使用中,等待一个被释放的资源或者一个新资源,handoffQueue里面获取 -
判断 poolEntity 是否被标记驱逐 或者 是否connectionAlive 以及上一次访问时间到当前是否 大于 aliveBypassWindowMs(默认 500ms)
-
如果是有效连接,便开始 创建createProxyConnection
-
否则 继续循环 2-3 的 步骤,直到 connectionTimeout 用完
三、归还Connection
逻辑代码主要在 HikariPool 类下面的 recycle(final PoolEntry poolEntry) ,流程图如下:
主要逻辑:
1.先将里面的所有Statement 关闭,并将保存的list 对象openStatements 清空
2.将 代理delegate 状态设置为 CLOSE_CONNECTION
3.将 poolEntity 的状态设置为 STATE_NOT_IN_USE
4.判断是否有 等待线程,如果有 直接推到 handoffQueue 队列 进行第一时间的交换
5.否则放入 本地 threadLocalList ,默认是弱引用,这样不影响回收
四、总结
Hikari 在获取连接的时候,做了一定的优化:
首先 查看 是否有可用的连接,从 ThreadLocal<List> 里面获取,这里 的 list ,是自定义了一个 FastList,而 FastList 和普通的ArrayList 不同之处就是 Fast 在get() 和remove () 方面去掉了rangeCheck(index); 一定程度上又快了一些.
全部资源用了 CopyOnWriteArrayList 定义的 sharedList ,这样读的是没有锁,写的时候进行了 加锁操作
SynchronousQueue 是使用了 公平锁 机制, SynchronousQueue 就是一定程度上无缓存的队列.
Hikari 作者 不推荐使用minimumIdle,该属性控制HikariCP尝试在池中维护的最小空闲连接数。如果空闲连接低于此值并且池中的总连接数少于maximumPoolSize,HikariCP将尽最大努力快速高效地添加其他连接。但是,为了获得最佳性能和响应尖峰需求,我们建议不要设置此值,而是允许HikariCP充当固定大小的连接池。
Hikari 作者 认为如果minimumIdle小于maximumPoolSize的话,在流量激增的时候需要额外的连接,此时在请求方法里头再去处理新建连接会造成性能损失,即会导致数据库一方面降低连接建立的速度,另一方面也会影响既有的连接事务的完成,间接影响了这些既有连接归还到连接池的速度。 Hikari 作者 认为minimumIdle与maximumPoolSize设置成一样,多余的空闲连接不会对整体的性能有什么严重影响。
支付宝 | 微信 |
---|---|
![]() | ![]() |
如果有帮助记得打赏哦 | 特别需要您的打赏哦 |