浅析HikariCP连接与关闭

浅析HikariCP连接与关闭

HikariCP 是一个高性能的 JDBC 连接池组件,号称性能最好的后起之秀,是一个基于BoneCP做了不少的改进和优化的高性能JDBC连接池。本文将从连接的获取和关闭进行分析。

1. 如何获取连接?

入口是HikariDataSource的getConnection()方法,内部调用HikariPool的getConnection()方法

HikariPool内部调用getConnection(final long hardTimeout)方法,其中hardTimeout就是配置中的connectionTimeout参数

java 复制代码
//HikariPool#getConnection
public Connection getConnection(final long hardTimeout) throws SQLException
   {
      
      final long startTime = currentTime();

      try {
         long timeout = hardTimeout;
         do {
            PoolEntry poolEntry = connectionBag.borrow(timeout, MILLISECONDS);
            if (poolEntry == null) {
               break; // We timed out... break and throw exception
            }

            final long now = currentTime();
            if (poolEntry.isMarkedEvicted() || (elapsedMillis(poolEntry.lastAccessed, now) > ALIVE_BYPASS_WINDOW_MS && !isConnectionAlive(poolEntry.connection))) {
               closeConnection(poolEntry, poolEntry.isMarkedEvicted() ? EVICTED_CONNECTION_MESSAGE : DEAD_CONNECTION_MESSAGE);
               timeout = hardTimeout - elapsedMillis(startTime);
            }
            else {
               return poolEntry.createProxyConnection(leakTaskFactory.schedule(poolEntry), now);
            }
         } while (timeout > 0L);

         throw createTimeoutException(startTime);
      }
      catch (InterruptedException e) {
       	//...
      }
}

很关键的是怎么从ConcurrentBag中获取到一个对象。

  1. 首先会看下当前线程下,是否有可用的连接,如果有的话,直接获取来用。线程私有对象,可以避免锁竞争。
java 复制代码
//ConcurrentBag#borrow
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);
		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. 其次会在共享的对象里,查看是否有可用的连接,如果有的话直接返回。
java 复制代码
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;
	}
}
  1. 如果都没有获取到对象,会触发一个通知,有个listener专门用于对象的创建。然后该线程内部会一直poll直到超时。 如果超时还没获取到对象,就直接返回。
java 复制代码
listener.addBagItem(waiting);
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);
return null;

在addBagItem方法中,会计算一下waiting减去带创建的数量,如果大于0才放到线程池中执行对象创建。addConnectionExecutor是只有一个线程的线程池。

java 复制代码
//HikariPool#addBagItem
public void addBagItem(final int waiting) {
	final boolean shouldAdd = waiting - addConnectionQueue.size() >= 0; // Yes, >= is intentional.
	if(shouldAdd) {
		addConnectionExecutor.submit(POOL_ENTRY_CREATOR);
	}
}

线程内部的处理逻辑如下:主要逻辑以下

  1. 是否继续创建
  2. 创建一个对象。
  3. 触发对象添加通知。
java 复制代码
//PoolEntryCreator#call
public Boolean call() throws Exception {
 	long sleepBackoff = 250 L;
 	while(poolState == POOL_NORMAL && shouldCreateAnotherConnection()) {
 		final PoolEntry poolEntry = createPoolEntry();
 		if(poolEntry != null) {
 			connectionBag.add(poolEntry);
 			LOGGER.debug("{} - Added connection {}", poolName, poolEntry.connection);
 			if(loggingPrefix != null) {
 				logPoolState(loggingPrefix);
 			}
 			return Boolean.TRUE;
 		}
 		// failed to get connection from db, sleep and retry
 		quietlySleep(sleepBackoff);
 		sleepBackoff = Math.min(SECONDS.toMillis(10), Math.min(connectionTimeout, (long)(sleepBackoff * 1.5)));
 	}
 	// Pool is suspended or shutdown or at max size
 	return Boolean.FALSE;
 }

是否继续执行逻辑如下:

  1. 当前总的连接 小于 最大连接数。
  2. 当前等待连接的线程>0 或者 当前空闲连接小于最小连接。
java 复制代码
//PoolEntryCreator#shouldCreateAnotherConnection
private boolean shouldCreateAnotherConnection() {
	return getTotalConnections() < config.getMaximumPoolSize() && (connectionBag.getWaitingThreadCount() > 0 || getIdleConnections() < config.getMinimumIdle());
}

内部逻辑:

  1. 创建一个PoolEntry对象。
  2. 如果有设置maxLifetime,则会在以maxLifetime的97.5%时长,添加一个延迟任务,用于关闭连接。(后面再重点分析一下)
java 复制代码
private PoolEntry createPoolEntry() {
	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.setFutureEol(houseKeepingExecutorService.schedule(
		   			() - > {
			if(softEvictConnection(poolEntry, "(connection has passed maxLifetime)", false 
			/* not owner */
			)) {
				addBagItem(connectionBag.getWaitingThreadCount());
			}
		}, lifetime, MILLISECONDS));
	}
	return poolEntry;
}

newPoolEntry方法内部逻辑如下:

java 复制代码
//PoolBase#newPoolEntry
PoolEntry newPoolEntry() throws Exception{
	return new PoolEntry(newConnection(), this, isReadOnly, isAutoCommit);
}

内部会调用dataSource的getConnection()方法。dataSource的实现是DriverDataSource,内部会调用driver进行连接。

java 复制代码
//PoolBase#newConnection
private Connection newConnection() throws Exception{
	final long start = currentTime();
	Connection connection = null;
	String username = config.getUsername();
	String password = config.getPassword();
	connection = (username == null) ? dataSource.getConnection() : dataSource.getConnection(username, password);
	if (connection == null) {
		throw new SQLTransientConnectionException("DataSource returned null unexpectedly");
	}
	setupConnection(connection);
	lastConnectionFailure.set(null);
	return connection;
}
java 复制代码
//DriverDataSource#getConnection
public Connection getConnection() throws SQLException{
	return driver.connect(jdbcUrl, driverProperties);
}

再看下创建对象后,怎么通知出去。

  • 首先先加入到共享对象池里。
  • 再加入队列中。注意这里的队列是:SynchronousQueue。在borrow方法中,会有一直poll。如果有对象add进去了,就能立马获取到对象。
java 复制代码
//ConcurrentBag#add
public void add(final T bagEntry){
	sharedList.add(bagEntry);
	// spin until a thread takes it or none are waiting
	while (waiters.get() > 0 && !handoffQueue.offer(bagEntry)) {
		yield();
	}
}

回到HikariPool中,connectionBag.borrow()请求已经返回。

  1. 如果borrow()返回为空,说明没有获取到对象,此时估计已经超时了。所以直接退出循环。
  2. 如果获取到了对象已经是标记"驱除" 或者 (距离上次访问超过500ms,且连接失效),就会关闭连接。
  3. 如果对象存在,就会创建一个代理连接对象返回。
java 复制代码
long timeout = hardTimeout;
do {
	PoolEntry poolEntry = connectionBag.borrow(timeout, MILLISECONDS);
	if (poolEntry == null) {
		break;
		// We timed out... break and throw exception
	}
	final long now = currentTime();
	if (poolEntry.isMarkedEvicted() || (elapsedMillis(poolEntry.lastAccessed, now) > ALIVE_BYPASS_WINDOW_MS && !isConnectionAlive(poolEntry.connection))) {
		closeConnection(poolEntry, poolEntry.isMarkedEvicted() ? EVICTED_CONNECTION_MESSAGE : DEAD_CONNECTION_MESSAGE);
		timeout = hardTimeout - elapsedMillis(startTime);
	} else {
		return poolEntry.createProxyConnection(leakTaskFactory.schedule(poolEntry), now);
	}
}while (timeout > 0L);
throw createTimeoutException(startTime);

最后在看下如果获取不到对象,最后如何抛出异常。

  • 会向获取上次连接失败原因。上次连接失败原因会在调用DriverDataSource中设置,如果连接成功,则会设置成null,如果捕获到异常,则会设置捕获到的异常。
  • 但是上次连接失败原因只有一个。而且创建连接和返回连接对象是异步的,所以感觉这里获取的连接失败原因,可能并不是当时的原因。假设jdbc连接超时时长是30s,但是从HikariPool中获取对象的超时时间为5s,当真连接不上数据库时,返回的并不是超时,而是之前的异常。当然某些情况下可能是准确的,如果连接失败后快速返回。
java 复制代码
private SQLException createTimeoutException(long startTime){
	
	String sqlState = null;
	final Throwable originalException = getLastConnectionFailure();
	if (originalException instanceof SQLException) {
		sqlState = ((SQLException) originalException).getSQLState();
	}
	final SQLException connectionException = new SQLTransientConnectionException(poolName + " - Connection is not available, request timed out after " + elapsedMillis(startTime) + "ms.", sqlState, originalException);
	if (originalException instanceof SQLException) {
		connectionException.setNextException((SQLException) originalException);
	}
	return connectionException;
}

2. 如何关闭连接?

2.1 连接维护

空闲连接维护:有三个场景

  1. 定时任务主动维护。
  2. 创建对象时,设置延迟任务用于标记清除。
  3. 从对象池中返回数据,查看是否标记,然后删除。

2.1.1 定时任务主动维护

在HikariPool创建的时候,会创建一个定时任务用于维护连接。 PoolEntry内部的Connection对象是:JDBC4Connection HikariPool返回的Connection的对象是:ProxyConnection,对用close方法时,会触发对象回收。

java 复制代码
//HikariPool#构造器
this.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, HOUSEKEEPING_PERIOD_MS, MILLISECONDS);

HouseKeeper内部逻辑如下:

  1. 判断是否有设置空闲时间、当前最小连接数 小于 最大连接数。
  2. 获取未使用连接的对象,减去最小连接数,为待关闭的连接。(如果设置最小连接,空闲时间,则最小连接不受空闲时间影响)
  3. 执行closeConnection方法。
  4. 填充对象池。
java 复制代码
final long idleTimeout = config.getIdleTimeout();
final long now = currentTime();
String afterPrefix = "Pool ";
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

closeConnection的逻辑如下:

  1. 会调用remove,这个会把对象从sharedList(共享对象池)中删除。
  2. 把包装对象给回收掉,poolEntry.close()。
  3. 调用quietlyCloseConnection,内部会最终会调用Connection的close方法,把连接给关闭掉。
java 复制代码
//HikariPool#closeConnection
void closeConnection(final PoolEntry poolEntry, final String closureReason) {
	if (connectionBag.remove(poolEntry)) {
		final Connection connection = poolEntry.close();
		closeConnectionExecutor.execute(() -> {
			quietlyCloseConnection(connection, closureReason);
			if (poolState == POOL_NORMAL) {
				fillPool();
			}
		}
		);
	}
}

在HouseKeeper run方法和HikariPool的closeConnection方法都会调用:fillPool()方法。

  1. 判断是否继续添加:最大连接数减去当前连接总数,最小连接数减去空闲数,取它们的最小值,然后再减去当前待添加连接对象队列数。
  2. 如果需要继续添加:那么就交给线程池创建。
java 复制代码
//HikariPool#fillPool
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);
	}
}

2.1.2 创建对象时,设置延迟任务用于标记清除。

在createPoolEntry方法中,内部会创建一个延迟任务,标记清除。 如果softEvictConnection调用成功,则会根据当前等待对象的数量,再创建对象。

java 复制代码
//HikariPool#createPoolEntry
poolEntry.setFutureEol(houseKeepingExecutorService.schedule(
               () -> {
	if (softEvictConnection(poolEntry, "(connection has passed maxLifetime)", false 
	/* not owner */
	)) {
		addBagItem(connectionBag.getWaitingThreadCount());
	}
},lifetime, MILLISECONDS));

softEvictConnection中:

  1. 先标记为待清除。
  2. 如果是当前创建的直接关闭连接,或者这连接空闲都会直接关闭连接。
java 复制代码
//HikariPool#softEvictConnection
private Boolean softEvictConnection(final PoolEntry poolEntry, final String reason, final Boolean owner) {
	poolEntry.markEvicted();
	if (owner || connectionBag.reserve(poolEntry)) {
		closeConnection(poolEntry, reason);
		return true;
	}
	return false;
}

处理createPoolEntry方法会创建一个延迟任务之外,还有几种情况也会调用softEvictConnection:

  1. HikariPool#evictConnection,用户主动调用驱逐连接。
  2. HouseKeeper,检测到时钟倒退,关闭所有连接。
  3. HikariDataSource#close,关闭连接池。

2.1.3 从对象池中返回数据,查看是否标记或连接失效,然后关闭连接。

  1. isMarkedEvicted()就会被定时任务检测空闲标记上。
  2. 如果设置了connectionTestQuery则执行这个SQL语句校验,否则直接用Connection接口的isValid进行校验。
java 复制代码
//HikariPool#getConnection
if (poolEntry.isMarkedEvicted() || (elapsedMillis(poolEntry.lastAccessed, now) > ALIVE_BYPASS_WINDOW_MS && !isConnectionAlive(poolEntry.connection))) {
	closeConnection(poolEntry, poolEntry.isMarkedEvicted() ? EVICTED_CONNECTION_MESSAGE : DEAD_CONNECTION_MESSAGE);
	timeout = hardTimeout - elapsedMillis(startTime);
}

2.2 对象回收

从对象池中返回的Connection会被封装为ProxyConnection,所以如果应用层面调用close方法为ProxyConnection的close方法。

在finally中,会调用PoolEntry的recycle方法。

java 复制代码
//ProxyConnection#close
public final void close() throws SQLException{
	// Closing statements can cause connection eviction, so this must run before the conditional below
	closeStatements();
	if (delegate != ClosedConnection.CLOSED_CONNECTION) {
		leakTask.cancel();
		try {
			//...
		}catch (SQLException e) {
			//...
		}finally {
			delegate = ClosedConnection.CLOSED_CONNECTION;
			poolEntry.recycle(lastAccess);
		}
	}
}

PoolEntry的recycle方法内部,又会调用HikariPool的recycle方法。

java 复制代码
//PoolEntry#recycle
void recycle(final long lastAccessed){
	if (connection != null) {
		this.lastAccessed = lastAccessed;
		hikariPool.recycle(this);
	}
}

HikariPool的recycle方法,会调用ConcurrentBag的requite方法:

java 复制代码
//HikariPool#recycle
void recycle(final PoolEntry poolEntry){
	metricsTracker.recordConnectionUsage(poolEntry);
	connectionBag.requite(poolEntry);
}

requite方法的逻辑如下:

  1. 先设置对象状态为未使用。

  2. 查看是否有等待连接对象的请求者,如果有则直接给等待的请求者。

  3. 如果没有等待连接对象的请求者,那么就会加入到线程本地变量。那么就会是,哪个线程调用了ProxyConnection的close方法,就会把这个对象加入到自己的本地线程变量。后面线程再从HikariPool中获取连接对象时,直接返回本地线程中的对象,可以减少锁竞争。

    java 复制代码
    //ConcurrentBag#requite
    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();
    	threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry);
    }

3. 参考资料

  • HikariCP源码
相关推荐
许野平17 分钟前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
duration~32 分钟前
Maven随笔
java·maven
zmgst36 分钟前
canal1.1.7使用canal-adapter进行mysql同步数据
java·数据库·mysql
跃ZHD1 小时前
前后端分离,Jackson,Long精度丢失
java
blammmp1 小时前
Java:数据结构-枚举
java·开发语言·数据结构
暗黑起源喵1 小时前
设计模式-工厂设计模式
java·开发语言·设计模式
WaaTong1 小时前
Java反射
java·开发语言·反射
齐 飞2 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
九圣残炎2 小时前
【从零开始的LeetCode-算法】1456. 定长子串中元音的最大数目
java·算法·leetcode
wclass-zhengge2 小时前
Netty篇(入门编程)
java·linux·服务器