前言
TomcatJdbc数据库连接池中的连接,会在被使用的各个阶段进行校验,以确保连接是一个有效可用的连接。
本文将结合TomcatJdbc 连接校验相关配置和源码,对连接校验的原理进行学习,Tomcat 版本为9.0.82。
TomcatJdbc往期文章:
接口访问超时引发的思考
搞懂TomcatJdbc之连接池初始化
搞懂TomcatJdbc之连接获取
正文
一. 连接校验源码解析
TomcatJdbc 数据库连接池中的连接,类型为PooledConnection ,其持有一个真实的数据库物理连接java.sql.Connection 。PooledConnection 提供了一组公共方法名为validate() ,用于对当前PooledConnection 进行校验,而validate() 方法也正是TomcatJdbc 数据库连接池中的连接的校验的统一入口 ,即无论哪种情况下的校验,均会调用到PooledConnection#validate 方法。下面看一下PooledConnection#validate方法的实现,如下所示。
java
public boolean validate(int validateAction) {
return validate(validateAction, null);
}
继续跟进重载的validate() 方法,如下所示。
java
// 这里validateAction可以为如下值:
// VALIDATE_BORROW(1),表示获取连接校验
// VALIDATE_RETURN(2),表示归还连接校验
// VALIDATE_IDLE(3),表示空闲连接校验
// VALIDATE_INIT(4),表示初始化连接校验
public boolean validate(int validateAction, String sql) {
// 被丢弃的连接直接校验失败
if (this.isDiscarded()) {
return false;
}
// 判断当前的校验类型是否有必要进行
// 没必要进行的校验则直接校验通过
if (!doValidate(validateAction)) {
return true;
}
// 在配置了校验间隔以及校验类型不是初始化连接校验的情况下
// 需要判断当前距离连接上一次校验的时间是否小于校验间隔
// 若小于则直接校验通过
long now = System.currentTimeMillis();
if (validateAction!=VALIDATE_INIT &&
poolProperties.getValidationInterval() > 0 &&
(now - this.lastValidated) <
poolProperties.getValidationInterval()) {
return true;
}
// 若配置了Validator则使用Validator来校验连接
if (poolProperties.getValidator() != null) {
if (poolProperties.getValidator().validate(connection, validateAction)) {
this.lastValidated = now;
return true;
} else {
if (getPoolProperties().getLogValidationErrors()) {
log.error("Custom validation through "+poolProperties.getValidator()+" failed.");
}
return false;
}
}
// 通常不会配置Validator
// 所以通过执行query语句进行连接校验
String query = sql;
// 如果是初始化连接校验且配置了InitSQL
// 则query语句使用配置的InitSQL
if (validateAction == VALIDATE_INIT && poolProperties.getInitSQL() != null) {
query = poolProperties.getInitSQL();
}
if (query == null) {
// 否则使用配置的校验连接的SQL
query = poolProperties.getValidationQuery();
}
......
boolean transactionCommitted = false;
Statement stmt = null;
try {
// 通过物理连接创建出Statement
stmt = connection.createStatement();
// 获取配置的校验连接超时时间
// 默认的校验连接超时时间为-1
int validationQueryTimeout = poolProperties.getValidationQueryTimeout();
if (validationQueryTimeout > 0) {
stmt.setQueryTimeout(validationQueryTimeout);
}
// 执行校验SQL
stmt.execute(query);
stmt.close();
this.lastValidated = now;
transactionCommitted = silentlyCommitTransactionIfNeeded();
return true;
} catch (Exception ex) {
......
} finally {
if (!transactionCommitted) {
silentlyRollbackTransactionIfNeeded();
}
}
return false;
}
上述校验流程总结如下。
-
先判断被校验连接是否已经被丢失。通过判断连接的discarded 字段是否为true,来判断连接是否已经被丢弃,被丢弃的连接直接校验失败;
-
然后判断当前校验类型是否需要进行。validate() 方法作为如下四种校验的统一校验入口,需要根据validate() 方法的入参validateAction以及配置来判断当前校验是否需要进行:
一. VALIDATE_BORROW (1)。表示获取连接校验,validateAction 等于VALIDATE_BORROW ,且将testOnBorrow 配置为true 时校验可以进行;
二. VALIDATE_RETURN (2)。表示归还连接校验,validateAction 等于VALIDATE_RETURN ,且将testOnReturn 配置为true 时校验可以进行;
三. VALIDATE_IDLE (3)。表示空闲连接校验,validateAction 等于VALIDATE_IDLE ,且将testWhileIdle 配置为true 时校验可以进行;
四. VALIDATE_INIT (4)。表示初始化连接校验,validateAction 等于VALIDATE_INIT ,且将testOnConnect 配置为true时校验可以进行。
校验被判断不需要进行时,直接返回true,表示校验通过;
-
然后判断连接是否满足校验间隔。在配置了校验间隔validationInterval 以及校验类型不是初始化连接校验VALIDATE_INIT的情况下,需要判断当前距离连接上一次校验的时间是否小于校验间隔,若小于则表示校验过于频繁,此时直接校验通过,若大于等于则允许校验;
-
然后确定校验连接的方式。如果配置了Validator ,则使用Validator 来校验连接,但通常不会配置Validator ,此时通过执行query 语句来校验连接。query 语句在初始化连接校验场景下且配置了InitSQL 时取值为InitSQL ,否则query 就取配置的validationQuery ,如果没有配置validationQuery ,此时会使用Springboot 提供的DatabaseDriver 里面预置的校验语句:/ * ping */ SELECT 1;
-
最后使用连接来执行query。如果执行成功,则校验成功,否则校验失败。
流程图如下所示。
二. 连接校验的场景
在上一节中已经知道,在TomcatJdbc中有如下四种情况会触发连接校验。
- 获取连接时且将testOnBorrow 配置为true;
- 归还连接时且将testOnReturn 配置为true;
- 连接空闲时且将testWhileIdle 配置为true;
- 创建连接时且将testOnConnect 配置为true。
那么本节将说明一下如上的几种连接的校验场景。
1. 获取连接校验
当配置testOnBorrow 为true时,每次从连接池获取连接都会校验一下连接,简化版源码如下所示。
java
protected PooledConnection borrowConnection(long now, PooledConnection con, String username, String password) throws SQLException {
boolean setToNull = false;
try {
......
if (!forceReconnect) {
// 在这里校验连接
if ((!con.isDiscarded()) && con.validate(PooledConnection.VALIDATE_BORROW)) {
......
return con;
}
}
// 执行到这里说明连接检验失败
try {
// 重新连接一次数据库得到物理连接
con.reconnect();
reconnectedCount.incrementAndGet();
int validationMode = isInitNewConnections() ?
PooledConnection.VALIDATE_INIT:
PooledConnection.VALIDATE_BORROW;
if (con.validate(validationMode)) {
......
return con;
} else {
......
}
} catch (Exception x) {
......
}
} finally {
......
}
}
通过上面源码还能知道,当连接校验不通过时,会重新去连接一次数据库,然后当前连接持有的物理连接就替换为重新建立的连接。
2. 归还连接校验
当配置testOnReturn 为true 时,将连接归还到连接池会校验一下连接。连接的归还,对应方法为ConnectionPool#returnConnection,简化版源码如下所示。
java
protected void returnConnection(PooledConnection con) {
......
if (con != null) {
try {
......
// 将连接从busy队列移除
if (busy.remove(con)) {
// 在shouldClose()会进行归还连接校验
if (!shouldClose(con,PooledConnection.VALIDATE_RETURN) && reconnectIfExpired(con)) {
......
} else {
// 归还连接校验不通过时需要释放底层物理连接
release(con);
}
} else {
......
release(con);
}
} finally {
con.unlock();
}
}
}
在将连接归还到数据库连接池时,会调用到shouldClose() 方法来判断连接底层的物理连接是否需要关闭,在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;
}
如果归还连接校验失败,则shouldClose() 方法会返回false ,从而会调用到ConnectionPool#release方法来释放底层物理连接。
3. 空闲连接校验
当配置testWhileIdle 为true 时,TomcatJdbc 会在一个定时任务中对idle队列中的连接进行校验。定时任务的执行逻辑如下所示。
java
public void run() {
ConnectionPool pool = this.pool.get();
if (pool == null) {
stopRunning();
} else if (!pool.isClosed()) {
try {
if (pool.getPoolProperties().isRemoveAbandoned()
|| pool.getPoolProperties().getSuspectTimeout() > 0) {
pool.checkAbandoned();
}
if (pool.getPoolProperties().getMinIdle() < pool.idle
.size()) {
pool.checkIdle();
}
if (pool.getPoolProperties().isTestWhileIdle()) {
// 在这里校验链接
pool.testAllIdle(false);
} else if (pool.getPoolProperties().getMaxAge() > 0) {
pool.testAllIdle(true);
}
} catch (Exception x) {
log.error("", x);
}
}
}
上述的连接校验,如果校验不通过,则连接对应的底层物理连接会被释放,如果校验通过,就相当于对连接做了一次保活操作。
4. 创建连接校验
当配置testOnConnect 为true 时,每次创建新连接时,都会进入到连接校验逻辑,对应方法为ConnectionPool#createConnection,简化版源码如下所示。
java
protected PooledConnection createConnection(long now, PooledConnection notUsed, String username, String password) throws SQLException {
// 创建一个空连接
// 即底层物理连接还没建立
PooledConnection con = create(false);
......
boolean error = false;
try {
con.lock();
// 实际建立底层物理连接
con.connect();
// 在这里执行创建连接校验
if (con.validate(PooledConnection.VALIDATE_INIT)) {
......
return con;
} else {
throw new SQLException("Validation Query Failed, enable logValidationErrors for more details.");
}
} catch (Exception e) {
......
} finally {
......
}
}
如果校验通过,就返回这个新建立的连接,如果校验失败,则抛出异常告诉外层本次连接创建失败。
总结
TomcatJdbc 有着较为完善且灵活的连接校验机制,在获取连接 ,归还连接 ,创建连接 和连接空闲时都提供了配置来决定是否进行校验。
对于获取连接校验,开启条件是配置testOnBorrow 为true,校验不通过时,会重连一次数据库得到底层物理连接。
对于归还连接校验,开启条件是配置testOnReturn 为true,校验不通过时,会释放底层物理连接。
对于创建连接校验,开启条件是配置testOnConnect 为true,校验不通过时,本次连接创建会失败。
对于空闲连接校验,开启条件是配置testWhileIdle 为true,校验不通过时,底层物理连接会被释放,校验通过时,相当于对连接做了一次保活操作。
获取连接校验,归还连接校验以及创建连接校验,均是在业务线程中,所以通常推荐只开启空闲连接校验,让后台线程完成连接校验和保活的工作即可。