连接池 Druid (四) - 连接归还
轻车熟路,连接归还是通过Connection的代理对象重写close方法完成的,通过前面的学习我们已经知道Connectin的代理对象是DruidPooledConnection,所以我们直接看DruidPooledConnection的close方法。
DruidPooledConnection#close
直接上代码:
public void close() throws SQLException {
if (this.disable) {
return;
}
DruidConnectionHolder holder = this.holder;
if (holder == null) {
if (dupCloseLogEnable) {
LOG.error("dup close");
}
return;
}
DruidAbstractDataSource dataSource = holder.getDataSource();
boolean isSameThread = this.getOwnerThread() == Thread.currentThread();
if (!isSameThread) {
dataSource.setAsyncCloseConnectionEnable(true);
}
if (dataSource.isAsyncCloseConnectionEnable()) {
syncClose();
return;
}
判断当前Connection(DruidPooledConnection)的状态为disbale、或者connectionHolder(DruidConnectionHolder)为null(说明连接已关闭了),则直接返回。
然后判断连接的ownerThread(获取connection的线程)与当前线程不是同一线程的话,则设置异步关闭asyncCloseConnectionEnable=true。调用syncClose()方法。
syncClose()方法和同线程关闭方式的代码逻辑基本一致,只不过syncClose()整个方法需要加锁,同线程关闭则不需要。
所以我们就不贴出syncClose()方法的源码了。
在什么场景下数据库连接会跨线程关闭?一个线程获取到数据库连接,然后会交给另外一个线程,由另外一个线程执行连接关闭?应用层这么做是为了实现多线程之间的事务处理?
接下来:
if (!CLOSING_UPDATER.compareAndSet(this, 0, 1)) {
return;
}
try {
for (ConnectionEventListener listener : holder.getConnectionEventListeners()) {
listener.connectionClosed(new ConnectionEvent(this));
}
List<Filter> filters = dataSource.getProxyFilters();
if (filters.size() > 0) {
FilterChainImpl filterChain = new FilterChainImpl(dataSource);
filterChain.dataSource_recycle(this);
} else {
recycle();
}
} finally {
CLOSING_UPDATER.set(this, 0);
}
this.disable = true;
}
CAS方式修改当前对象的closing属性值为1(0->1),如果修改不成功,则说明有其他线程正在试图关闭当前连接,直接返回。
获取ConnectionHolder的所有ConnectionEventListener对象,给所有监听对象发送连接关闭通知。
然后,获取dataSource的ProxyFilters,不空的话调用filterChain的dataSource_recycle方法,有关dataSource过滤器我们依然暂时不管。没有filter的话,调用recycle()回收连接,之后CAS方法修改closing属性值为0,并设置当前连接对象的disable=true。
所以,我们发现连接归还应该是recycle()方法中实现的。
DruidPooledConnection#recycle()
连接(DruidPooledConnection)的recycle方法:
public void recycle() throws SQLException {
if (this.disable) {
return;
}
DruidConnectionHolder holder = this.holder;
if (holder == null) {
if (dupCloseLogEnable) {
LOG.error("dup close");
}
return;
}
if (!this.abandoned) {
DruidAbstractDataSource dataSource = holder.getDataSource();
dataSource.recycle(this);
}
this.holder = null;
conn = null;
transactionInfo = null;
closed = true;
}
如果当前连接没有被遗弃(abandoned)的话,调用dataSource的recycle方法回收,之后清理相关对象、设置close为true。
abandoned是在removeAbandoned参数打开、连接执行时长超过设定的超时时长removeAbandonedTimeoutMillis之后设置的。有关removeAbandoned我们后面会专门进行分析,此处略过。
那接下来就是DataSource的recycle方法了。
DruidDataSrouce#recycle
代码比较长,我们还是分段分析:
/**
* 回收连接
*/
protected void recycle(DruidPooledConnection pooledConnection) throws SQLException {
final DruidConnectionHolder holder = pooledConnection.holder;
if (holder == null) {
LOG.warn("connectionHolder is null");
return;
}
if (logDifferentThread //
&& (!isAsyncCloseConnectionEnable()) //
&& pooledConnection.ownerThread != Thread.currentThread()//
) {
LOG.warn("get/close not same thread");
}
final Connection physicalConnection = holder.conn;
if (pooledConnection.traceEnable) {
Object oldInfo = null;
activeConnectionLock.lock();
try {
if (pooledConnection.traceEnable) {
oldInfo = activeConnections.remove(pooledConnection);
pooledConnection.traceEnable = false;
}
} finally {
activeConnectionLock.unlock();
}
if (oldInfo == null) {
if (LOG.isWarnEnabled()) {
LOG.warn("remove abandonded failed. activeConnections.size " + activeConnections.size());
}
}
}
吐槽一句,终于出现了哪怕是一句话的javaDoc说明:回收连接。整个Druid连接池的源码中,都非常吝啬,少有注释。
判断当前连接DruidPooledConnection的traceEnable属性为true的话,加activeConnectionLock锁之后,将当前连接从activeConnections中移除。
traceEnable属性其实反应的还是removeAbandoned参数(这个参数虽然在正式环境不建议打开,但是代码中阴魂不散,到处都有)。removeAbandoned参数打开的情况下,获取连接的时候会将连接放入activeConnections中,并同时设置连接的traceEnable为true。所以我们其实可以认为这个traceEnable其实就是removeAbandoned参数,换了个名字而已。
所以这里的逻辑是:removeAbandoned参数打开的话,连接归还的时候如果该连接没有被abandoned的话,在归还连接时会将当前连接从activeConnections中移除,该连接就会避免被abandoned处理。
接下来:
final boolean isAutoCommit = holder.underlyingAutoCommit;
final boolean isReadOnly = holder.underlyingReadOnly;
final boolean testOnReturn = this.testOnReturn;
try {
// check need to rollback?
if ((!isAutoCommit) && (!isReadOnly)) {
pooledConnection.rollback();
}
boolean isSameThread = pooledConnection.ownerThread == Thread.currentThread();
if (!isSameThread) {
final ReentrantLock lock = pooledConnection.lock;
lock.lock();
try {
holder.reset();
} finally {
lock.unlock();
}
} else {
holder.reset();
}
if (holder.discard) {
return;
}
if (phyMaxUseCount > 0 && holder.useCount >= phyMaxUseCount) {
discardConnection(holder);
return;
}
if (physicalConnection.isClosed()) {
lock.lock();
try {
if (holder.active) {
activeCount--;
holder.active = false;
}
closeCount++;
} finally {
lock.unlock();
}
return;
}
对没有提交的事务做回滚处理。之后调用DruidConnectionHolder的reset方法,重新设置连接属性为默认值,因为连接获取之后应用从根据需要可能对连接属性重新进行了设置,归还的时候重新设置会默认值,以便每一次应用获取连接之后都能拿到各项属性设置为默认值的连接、而不是不确定。
这里对没有提交的事务的判断条件是!isAutoCommit,获取的是物理连接Connection的isAutocommit属性。我们知道如果应用开启事务管理的话,获取连接之后会设置连接的autoCommit为false,事务提交或回滚之后,一般情况下应用也会恢复连接的默认设置,这个时候autoCommit就会被恢复为true。比如Spring的事务框架在事务提交之后会通过TransactionManager的cleanupAfterCompletion->doCleanupAfterCompletion设置autoCommit为true。所以正常来讲,应用已经提交事务之后,autoCommit就会变为true,也就不需要Druid在归还连接的时候处理回滚了。
Druid增加这个判断可能是为了给应用擦屁股(猜测而已),应用层没有启用事务框架的情况下手动开启了事务、设置autoCommit为false,执行完成之后没有重置autoCommit,这个时候应用可能commit、也可能没有commit事务,Durid的处理方式是:一律回滚。
仔细检查了HikariPool的连接回收过程,并没有这个回滚处理。个人不赞成(基于猜测而已………)Druid的这个回滚处理,因为事务提交还是回滚终究还是应用应该关注的事情,应用层为了简化处理逻辑可以把事务管理交给框架,比如Spring事务框架来处理。也就是说,要么是应用层自己解决、要么交给事务框架解决事务的提交或回滚是比较合理的选择。
继续。
检查如果DruidConnectionHolder已经被弃用(连接已经被弃用),直接返回,不归还。
检查如果物理连接已经被关闭,则加锁修改当前holder的active状态为false,直接返回,不归还。
然后:
if (testOnReturn) {
boolean validate = testConnectionInternal(holder, physicalConnection);
if (!validate) {
JdbcUtils.close(physicalConnection);
destroyCountUpdater.incrementAndGet(this);
lock.lock();
try {
if (holder.active) {
activeCount--;
holder.active = false;
}
closeCount++;
} finally {
lock.unlock();
}
return;
}
}
if (holder.initSchema != null) {
holder.conn.setSchema(holder.initSchema);
holder.initSchema = null;
}
if (!enable) {
discardConnection(holder);
return;
}
检查testOnReturn参数如果设置为true,则测试连接可用性,如果连接不可用,处理方式同上,修改holder的active=false,直接返回,不归还。
testOnReturn参数也没有必要设置,道理和testOnBorrow参数一样,会影响性能。这两个参数的默认设置都是false,不打开。
之后检查当前dataSrouce如果不可用,则调用discardConnection关闭连接,不归还。
然后:
boolean result;
final long currentTimeMillis = System.currentTimeMillis();
if (phyTimeoutMillis > 0) {
long phyConnectTimeMillis = currentTimeMillis - holder.connectTimeMillis;
if (phyConnectTimeMillis > phyTimeoutMillis) {
discardConnection(holder);
return;
}
}
lock.lock();
try {
if (holder.active) {
activeCount--;
holder.active = false;
}
closeCount++;
result = putLast(holder, currentTimeMillis);
recycleCount++;
} finally {
lock.unlock();
}
if (!result) {
JdbcUtils.close(holder.conn);
LOG.info("connection recyle failed.");
}
} catch (Throwable e) {
holder.clearStatementCache();
if (!holder.discard) {
discardConnection(holder);
holder.discard = true;
}
LOG.error("recyle error", e);
recycleErrorCountUpdater.incrementAndGet(this);
}
}
如果设置了phyTimeoutMillis(默认设置为-1,不检查)的话,检查当前连接创建以来的时长是否超过了该参数的设置,超过的话关闭连接,不归还。这个参数也不建议设置。
之后,加锁,调用putLast(holder, currentTimeMillis);归还连接,如果归还失败,则调用JdbcUtil.close方法,底层直接关闭连接。
putLast是归还连接的核心方法。
DruidDataSource#putLast
putLast是连接归还的最后一步,目的是各种检查校验、连接属性重置之后,最终将连接放回到连接池connections中。
putLast是在锁状态下执行的。
看代码:
boolean putLast(DruidConnectionHolder e, long lastActiveTimeMillis) {
if (poolingCount >= maxActive || e.discard || this.closed) {
return false;
}
e.lastActiveTimeMillis = lastActiveTimeMillis;
connections[poolingCount] = e;
incrementPoolingCount();
if (poolingCount > poolingPeak) {
poolingPeak = poolingCount;
poolingPeakTime = lastActiveTimeMillis;
}
notEmpty.signal();
notEmptySignalCount++;
return true;
}
检查如果没必要归还的话,比如连接池数量大于参数设定的maxActive数量、或者当前连接已经被遗弃、或者当前dataSource已经被关闭,则不需要归还,直接返回。
将连接放入到connections的尾部,之后增加连接池数量poolingCount。
调用notEmpty.signal();唤醒等待获取连接的线程:连接池中有新的连接加入,可以获取了。
Druid关闭连接(归还连接)代码分析完毕!
小结
Druid连接池的初始化、连接获取以及连接归还的源码分析完毕,后面会补充一篇文章分析连接遗弃的处理。
Thanks a lot!
上一篇 连接池 Druid (三) - 获取连接 getConnection
下一篇 连接池 Druid (补充) - removeAbandoned