浅析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中获取到一个对象。
- 首先会看下当前线程下,是否有可用的连接,如果有的话,直接获取来用。线程私有对象,可以避免锁竞争。
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;
}
}
}
- 其次会在共享的对象里,查看是否有可用的连接,如果有的话直接返回。
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;
}
}
- 如果都没有获取到对象,会触发一个通知,有个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);
}
}
线程内部的处理逻辑如下:主要逻辑以下
- 是否继续创建
- 创建一个对象。
- 触发对象添加通知。
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;
}
是否继续执行逻辑如下:
- 当前总的连接 小于 最大连接数。
- 当前等待连接的线程>0 或者 当前空闲连接小于最小连接。
java
//PoolEntryCreator#shouldCreateAnotherConnection
private boolean shouldCreateAnotherConnection() {
return getTotalConnections() < config.getMaximumPoolSize() && (connectionBag.getWaitingThreadCount() > 0 || getIdleConnections() < config.getMinimumIdle());
}
内部逻辑:
- 创建一个PoolEntry对象。
- 如果有设置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()请求已经返回。
- 如果borrow()返回为空,说明没有获取到对象,此时估计已经超时了。所以直接退出循环。
- 如果获取到了对象已经是标记"驱除" 或者 (距离上次访问超过500ms,且连接失效),就会关闭连接。
- 如果对象存在,就会创建一个代理连接对象返回。
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 连接维护
空闲连接维护:有三个场景
- 定时任务主动维护。
- 创建对象时,设置延迟任务用于标记清除。
- 从对象池中返回数据,查看是否标记,然后删除。
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内部逻辑如下:
- 判断是否有设置空闲时间、当前最小连接数 小于 最大连接数。
- 获取未使用连接的对象,减去最小连接数,为待关闭的连接。(如果设置最小连接,空闲时间,则最小连接不受空闲时间影响)
- 执行closeConnection方法。
- 填充对象池。
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的逻辑如下:
- 会调用remove,这个会把对象从sharedList(共享对象池)中删除。
- 把包装对象给回收掉,poolEntry.close()。
- 调用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()方法。
- 判断是否继续添加:最大连接数减去当前连接总数,最小连接数减去空闲数,取它们的最小值,然后再减去当前待添加连接对象队列数。
- 如果需要继续添加:那么就交给线程池创建。
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中:
- 先标记为待清除。
- 如果是当前创建的直接关闭连接,或者这连接空闲都会直接关闭连接。
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:
- HikariPool#evictConnection,用户主动调用驱逐连接。
- HouseKeeper,检测到时钟倒退,关闭所有连接。
- HikariDataSource#close,关闭连接池。
2.1.3 从对象池中返回数据,查看是否标记或连接失效,然后关闭连接。
- isMarkedEvicted()就会被定时任务检测空闲标记上。
- 如果设置了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方法的逻辑如下:
-
先设置对象状态为未使用。
-
查看是否有等待连接对象的请求者,如果有则直接给等待的请求者。
-
如果没有等待连接对象的请求者,那么就会加入到线程本地变量。那么就会是,哪个线程调用了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源码