获取数据库连接是通过DataSource发起的,如果应用使用HikariPool作为连接池的话,需要配置DataSource为HikariDataSource,应用通过调用HikariDataSource的getConnection方法获取数据库连接。
关闭数据库连接是直接调用获取到的数据库连接对象(Connection对象)的close方法完成的。
今天要研究的课题:获取及关闭数据库连接,获取数据库连接其实就是从HikariDataSource的getConnection方法入手,直到getConnection方法获取到数据库连接对象。然后再研究数据库连接对象的close方法。
获取数据库连接
直接看HikariDataSource的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;
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());
}
}
}
fastPathPool在HikariDataSource实例化的时候被初始化为HikariPool对象。如果fastPathPool在getConnection方法调用的时候尚未初始化,就在getConnection方法内完成初始化。之后再调用HikariPool对象的getConnection方法。
HikariPool#getConnection方法
直接看代码:
public Connection getConnection() throws SQLException
{
return getConnection(connectionTimeout);
}
直接调用了getConnection(connectionTimeout)方法:
public Connection getConnection(final long hardTimeout) throws SQLException
{
suspendResumeLock.acquire();
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) > 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);
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.acquire();方法,suspendResumeLock的主要作用是限流,初始化的时候设置最大并发线程数为MAX_PERMITS = 10000,如果同时获取连接的线程未超过限制数量的话,允许获取连接,否则,挂起、排队(内部通过Semaphore类实现,Semaphore是AQS的扩展实现)等待其他线程获取到连接、释放suspendResumeLock之后,才能继续获取到数据库连接。
然后调用connectionBag的borrow方法(稍后分析源码),从数据库连接池中获取连接。
如果没能从数据库连接池获取到连接的话,抛异常。
检查从连接池获取到的连接,如果连接被标记为Evicted(该连接由于超时或其他原因应该被回收),或者最后一次访问时间到当前时间之间的时间差超过了aliveBypassWindowMs的设置并且尝试后发现该连接已经失效(连不上了),则关闭该连接,timeout尚未超时的话重新调用connectionBag的borrow方法获取连接,直到超时或获取到可用的连接。
如果连接可用,则调用metricsTracker.recordBorrowStats,之后返回:
return poolEntry.createProxyConnection(leakTaskFactory.schedule(poolEntry), now);
注意最终返回的并不是java.sql.Connection对象,而是通过poolEnry.createProxyConnection方法创建的代理对象!
最终通过JavassistProxyFactory创建代理对象HikariProxyConnection。
返回代理数据库连接对象的同时,创建leakTask。leakTask对象的作用是:如果leakDetectionThreshold不为0的话,创建一个leakTask任务,在leakDetectionThreshold时长之后执行该任务抛出数据库连接泄露异常。该任务不被执行(不发生数据库泄露异常)的唯一条件就是在leakDetectionThreshold时长范围内,数据库连接成功执行完SQL语句后通过调用代理Connection对象的close方法取消该任务。
OK,getConnection方法源码分析完成了,目前为止,我们知道:
- 通过connectionBag.borrow方法从连接池获取连接数据库连接的封装对象poolEntry
- 最终获取到的数据库连接是代理对象HikariProxyConnection
下面我们首先分析一下connectionBag.borrow方法,borrow方法才是从连接池获取连接的关键。
connectionBag#borrow方法
上代码,分段分析,先看第一部分:
public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException
{
// Try the thread-local list first
final List
首先从threadList中获取,如果能获取到空闲的数据库连接、并且能成功通过CAS操作修改连接状态(从STATE_NOT_IN_USE修改为STATE_IN_USE)的话,直接返回。
threadList是ThreadLocal对象,当前线程如果已经获取过连接,在使用完归还给连接池的过程中,该连接会写入threadList。如果当前线程再次获取连接的话,就可以从threadList中获取,肯定是性能最好的获取方式。
接下来,如果threadList中没有获取到:
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;
}
}
获取到等待获取连接的线程数waiters,加1。
遍历sharedList,如果sharedList中有空闲连接,并且能够成功修改状态的话,说明获取成功,直接返回。
从sharedList获取连接成功的话,还要检查一下当前是否还有其他等待获取连接的线程(waiter>1),如果有的话说明连接池中的连接不太够用,则调用listener.addBagItem(waiting – 1)增加连接。listerner就是HikariPool对象,调用的其实就是HikariPool的addBagItem方法。
addBagItem方法的代码非常简单,就不贴出了,逻辑也很简单:等待获取连接的线程数如果大于等于等待加入连接池的连接数的话,就通过addConnectionExecutor增加连接到连接池中,否则,不需要增加,只需要稍等一下,addConnectionExecutor就会把等待队列中的连接加入池中。
继续看源码,如果从sharedList中也没有获取到连接的话:
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;
}
finally {
waiters.decrementAndGet();
}
}
调用listener.addBagItem(waiting),方法逻辑已经分析过了。
从handoffQueue获取连接,如果获取到的连接状不是空闲状态(其他线程捷足先登了),在timeout时间范围内继续循环从handoffQueue获取。
如果handoffQueue中没有获取到(此时肯定等待超时了),则世界返回null。
否则,如果获取到了空闲连接,并且能够成功修改连接状态为STATE_IN_USE的话,返回该连接。
最终,不管获取成功与否,waiters减1。
borrow方法分析完成!我们知道从连接池获取连接的基本逻辑是:
- 首先从ThreadLocal变量threadList中获取
- 如果没有获取到的话,从sharedList中获取
- 还是没有获取到的话,最后才从handoffQueue中获取
源码已经分析的很清楚了,逻辑也解释的清晰明了。但是为什么要这么做?
首先从threadList中获取连接的逻辑比较容易理解,如果当前线程已经获取过连接,用完之后存入threadList中,再次获取的话直接返回threadList中的连接,效率肯定是最高的,性能最优。
但是从sharedList中获取不到的话,为什么还要再从handoffQueue中获取?什么情况下会发生从sharedList中获取不到、从handoffQueue中能获取到?
仔细分析一下sharedList以及handoffQueue的结构、以及连接加入连接池的逻辑,就能明白这么做的原因了。
连接加入连接池的过程为:首先加入sharedList,之后再加入handoffQueue。
假设获取连接的线程读取sharedList的时候,sharedList没有空闲的连接,此时假设有一个新创建的连接正好刚刚加入到sharedList中。这种情况下获取连接的线程就从sharedList中完美错过了新加入的连接。
然后新加入的连接会加入handoffQueue,handoffQueue是手递手队列,加入连接的线程会等待获取连接的线程从队列中拿走该连接,两个人一拍即合,互相满足需求,完成“手递手”任务。
这个场景下,HikariPool的handoffQueue的设计一定是性能最好的,肯定回比反复遍历sharedList的性能好很多。
从连接池中获取连接的过程,源码级分析完成!
关闭连接 HikariProxyConnection#close方法
开篇已经说过了,关闭连接其实就是调用代理连接对象HikariProxyConnection的close方法。close方法是在他的父类、抽象类ProxyConnection中定义的:
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 {
if (isCommitStateDirty && !isAutoCommit) {
delegate.rollback();
lastAccess = currentTime();
LOGGER.debug("{} - Executed rollback on connection {} due to dirty commit state on close().", poolEntry.getPoolName(), delegate);
}
if (dirtyBits != 0) {
poolEntry.resetConnectionState(this, dirtyBits);
lastAccess = currentTime();
}
delegate.clearWarnings();
}
catch (SQLException e) {
// when connections are aborted, exceptions are often thrown that should not reach the application
if (!poolEntry.isMarkedEvicted()) {
throw checkException(e);
}
}
finally {
delegate = ClosedConnection.CLOSED_CONNECTION;
poolEntry.recycle(lastAccess);
}
}
}
首先调用closeStatements方法,关闭该连接打开的Statement。
然后,调用leakTask.cancel();取消连接泄露检查任务。
之后,调用poolEntry.resetConnectionState重置连接属性,准备归还到连接池。
最后,调用poolEntry.recycle(lastAccess);方法名说明一切:回收连接。
poolEntry#recycle(lastAccess)
代码很简单:
void recycle(final long lastAccessed)
{
if (connection != null) {
this.lastAccessed = lastAccessed;
hikariPool.recycle(this);
}
}
记录lastAccessed后,调用hikariPool.recycle(this)。
hikariPool#recycle
重点来了,调用了connectionBag的requite方法。
@Override
void recycle(final PoolEntry poolEntry)
{
metricsTracker.recordConnectionUsage(poolEntry);
connectionBag.requite(poolEntry);
}
connectionBag#requite
requite负责归还PoolEntry对象到连接池:
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 {
Thread.yield();
}
}
首先将poolEntry对象设置为空闲状态(STATE_NOT_IN_USE)。
之后检查如果有等待获取连接的线程存在的话,帮助其快速拿到连接:
如果当前对象已经不再是空闲状态(已经被getConnection线程拿走了),则直接返回。
否则如果没被拿走的话,当前连接对象放入handoffQueue,阻塞等待,如果被getConnection线程从handoffQueue中拿走的话,返回。
否则,循环一定次数(0xff=256次)后挂起等待10秒,直到连接被等待获取连接的线程拿走。(这个地方不是很理解,为什么要这么做?因为关闭连接的调用方应该是应用线程,做完自己该做的事情、归还连接的时候,还要负责把连接交给下一个等待获取连接的线程,难道不会影响当前业务的快速返回吗?)
最后,如果threadList的容量小于50的话,将当前poolEntry对象放入threadList。
小结
HikariPool的初始化、连接创建及加入连接池、连接获取、连接归还等所有连接池相关的主要逻辑分析完毕。
Thanks a lot!
上一篇 连接池 HikariPool (一) – 基础框架及初始化过程
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net