前言
正所谓有借有还,当业务线程从TomcatJdbc 数据库连接池获取到连接并使用完毕后,随即就应该将借出的连接归还回去。本文将结合源码,分析TomcatJdbc 数据库连接池的连接的归还实现。Tomcat 版本为9.0.82。
TomcatJdbc 往期文章:
接口访问超时引发的思考
搞懂TomcatJdbc之连接池初始化
搞懂TomcatJdbc之连接获取
搞懂TomcatJdbc之连接校验
未曾设想过的JDK动态代理写法
正文
TomcatJdbc 数据库连接池里的连接的类型是PooledConnection ,但最终业务线程借出来的连接实际是一个代理连接对象,这是因为每一个连接从TomcatJdbc 数据库连接池中被获取出来时,都会调用ConnectionPool#setupConnection 方法来基于动态代理来应用拦截器的功能,我们配置的所有拦截器都会被构造成一个拦截器的责任链,这个责任链中除了尾节点的其余节点类型都是JdbcInterceptor ,尾节点的类型是ProxyConnection ,下面是JdbcInterceptor ,ProxyConnection 和PooledConnection之间的关系类图。
那么就很清晰了,拦截器的责任链实际就是InvocationHandler 的责任链,那么调用代理连接对象的方法时,就会调用到拦截器链,从而拦截器的逻辑会执行,同时拦截器链的尾节点类型是ProxyConnection ,而ProxyConnection 持有PooledConnection ,所以最终PooledConnection 会被调用到,下面就看一下ProxyConnection 的invoke() 方法的实现。
java
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (compare(ISCLOSED_VAL, method)) {
return Boolean.valueOf(isClosed());
}
if (compare(CLOSE_VAL, method)) {
// 代理close()方法
if (connection == null) {
return null;
}
PooledConnection poolc = this.connection;
this.connection = null;
// 将连接归还到连接池
pool.returnConnection(poolc);
return null;
} else if (compare(TOSTRING_VAL, method)) {
return this.toString();
} else if (compare(GETCONNECTION_VAL, method) && connection != null) {
// 代理getConnection()方法
return connection.getConnection();
} else if (method.getDeclaringClass().isAssignableFrom(XAConnection.class) && connection != null) {
try {
return method.invoke(connection.getXAConnection(), args);
} catch (Throwable t) {
if (t instanceof InvocationTargetException) {
throw t.getCause() != null ? t.getCause() : t;
} else {
throw t;
}
}
}
if (isClosed()) {
throw new SQLException("Connection has already been closed.");
}
if (compare(UNWRAP_VAL, method)) {
return unwrap((Class<?>)args[0]);
} else if (compare(ISWRAPPERFOR_VAL, method)) {
return Boolean.valueOf(this.isWrapperFor((Class<?>)args[0]));
}
try {
PooledConnection poolc = connection;
if (poolc != null) {
// 调用到底层物理连接
return method.invoke(poolc.getConnection(), args);
} else {
throw new SQLException("Connection has already been closed.");
}
} catch (Throwable t) {
if (t instanceof InvocationTargetException) {
throw t.getCause() != null ? t.getCause() : t;
} else {
throw t;
}
}
}
通过上述代码可以知道,外层从TomcatJdbc 数据库连接池拿到一个连接后,所有对连接的操作,均会由ProxyConnection 完成代理,在ProxyConnection 中会判断如何完成当前操作,例如本文讨论的连接归还,则是通过ConnectionPool#returnConnection方法完成,具体源码实现如下所示。
java
protected void returnConnection(PooledConnection con) {
if (isClosed()) {
// 如果连接池已经关闭则直接释放当前待归还的连接
release(con);
return;
}
if (con != null) {
try {
returnedCount.incrementAndGet();
con.lock();
// 如果当前归还的连接已经被标记为已放弃的可疑连接则打印一些记录日志
if (con.isSuspect()) {
if (poolProperties.isLogAbandoned() && log.isInfoEnabled()) {
log.info("Connection(" + con + ") that has been marked suspect was returned."
+ " The processing time is " + (System.currentTimeMillis() - con.getTimestamp()) + " ms.");
}
if (jmxPool != null) {
jmxPool.notify(org.apache.tomcat.jdbc.pool.jmx.ConnectionPool.SUSPECT_RETURNED_NOTIFICATION,
"Connection(" + con + ") that has been marked suspect was returned.");
}
}
// 将待归还连接从busy队列移除
if (busy.remove(con)) {
// 判断连接是否应该被直接释放
if (!shouldClose(con, PooledConnection.VALIDATE_RETURN) && reconnectIfExpired(con)) {
con.clearWarnings();
con.setStackTrace(null);
// 更新连接的时间戳并消除嫌疑
con.setTimestamp(System.currentTimeMillis());
// 1. 如果idle队列已经达到上限则释放连接
// 2. idle队列未达到上限则尝试将连接归还到idle队列
// 3. 如果归还失败则释放连接
if (((idle.size() >= poolProperties.getMaxIdle()) && !poolProperties.isPoolSweeperEnabled()) || (!idle.offer(con))) {
if (log.isDebugEnabled()) {
log.debug("Connection [" + con + "] will be closed and not returned to the pool, idle[" + idle.size() + "]>=maxIdle["
+ poolProperties.getMaxIdle() + "] idle.offer failed.");
}
release(con);
}
} else {
// 进入这里表示shouldClose()返回true
// 即连接应该被直接释放而不是归还到连接池中
if (log.isDebugEnabled()) {
log.debug("Connection [" + con + "] will be closed and not returned to the pool.");
}
release(con);
}
} else {
// 从busy队列移除失败则直接释放连接
if (log.isDebugEnabled()) {
log.debug("Connection [" + con + "] will be closed and not returned to the pool, busy.remove failed.");
}
release(con);
}
} finally {
con.unlock();
}
}
}
上述连接归还逻辑小结如下。
- 如果连接池已经关闭则直接释放连接。如果连接池已经关闭,则连接也没必要再归还到连接池,而释放连接,则是直接关闭底层的物理连接;
- 打印被标记为已放弃的可疑连接信息。可疑连接,即suspect 设置为true 的连接,只有在连接借出去且达到suspectTimeout 还没归还的连接会被设置suspect 为true;
- 将连接从busy队列移除。如果移除失败,则直接释放连接;
- 判断连接是否应该直接被释放而不是归还。这里的判断是通过调用shouldClose() 方法来判断的,如果该方法返回false,则直接释放连接;
- 判断idle队列是否已经满了。如果满了,则直接释放连接;
- 在idle 队列未满时将连接归还到idle队列。如果归还失败,则直接释放连接。
流程图如下所示。
最后再看一下上面提到的ConnectionPool#shouldClose方法,该方法用于判断待归还的连接是否需要直接被释放,源码实现如下所示。
java
protected boolean shouldClose(PooledConnection con, int action) {
if (con.getConnectionVersion() < getPoolVersion()) {
return true;
}
if (con.isDiscarded()) {
// 已经被连接池丢弃的连接需要直接释放
return true;
}
if (isClosed()) {
// 连接池已经被关闭则连接也需要直接释放
return true;
}
if (!con.validate(action)) {
// 配置了归还连接校验且校验不通过则连接需要直接释放
return true;
}
if (!terminateTransaction(con)) {
// 终止事务失败时连接需要直接释放
return true;
}
// 上述判断均通过时则连接不需要直接释放
return false;
}
其实ConnectionPool#shouldClose 方法中最重要的就是在配置testOnReturn 为true时会在连接归还到连接池前做一次连接校验,以确保归还的连接都是可用的连接。
总结
所谓借出连接,其实就是从TomcatJdbc 的idle 队列中获取一个连接,这个借出的连接在被业务线程使用的同时,也会同时被放在busy队列中。
那么归还连接,其实就是把连接从busy 队列移除然后再放回idle 队列,我们也可以通过配置testOnReturn 为true 来让连接被归还到idle队列前进行一次校验,校验失败的连接会直接被释放,以确保每次归还的连接都是一个可用的连接。