1.Druid提供的校验参数
Druid对于连接校验提供了六个参数,其中testOnBorrow
、testOnReturn
、testWhileIdle
是Druid所提供的连接校验时点,其余三个参数为校验的相关配置。
配置 | 缺省值 | 说明 |
validationQuery | 用来检测连接是否有效的sql,要求是一个查询语句,常用select ‘x’。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。 | |
validationQueryTimeout | 单位:秒,检测连接是否有效的超时时间。底层调用jdbc Statement对象的void setQueryTimeout(int seconds)方法 | |
testOnBorrow | false | 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 |
testOnReturn | false | 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 |
testWhileIdle | true | 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 |
timeBetweenEvictionRunsMillis | 1分钟 | 有两个含义: (1) Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。 (2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明 |
2. Druid是如何进行连接校验的?
Druid依靠testConnectionInternal
方法进行连接校验,如果当前连接池存在validConnectionChecker
,则会利用validConnectionChecker
进行校验,否则将利用一些内部状态以及执行校验SQL的方式进行探活,主要的校验方式都是以执行校验SQL进行校验,而MySQL可以使用PingMethod
校验连接的有效性。
/**
* 校验连接是否有效
* @param holder
* @param conn
* @return
*/
protected boolean testConnectionInternal(DruidConnectionHolder holder, Connection conn) {
//此部分似乎是为了做监控用的
String sqlFile = JdbcSqlStat.getContextSqlFile();
String sqlName = JdbcSqlStat.getContextSqlName();
if (sqlFile != null) {
JdbcSqlStat.setContextSqlFile(null);
}
if (sqlName != null) {
JdbcSqlStat.setContextSqlName(null);
}
try {
//判断当前连接池所持有的检查器是否为空
if (validConnectionChecker != null) {
//调用检查器的教研方法进行教研
boolean valid = validConnectionChecker.isValidConnection(conn, validationQuery, validationQueryTimeout);
long currentTimeMillis = System.currentTimeMillis();
//设置相关的监控参数
if (holder != null) {
holder.lastValidTimeMillis = currentTimeMillis;
holder.lastExecTimeMillis = currentTimeMillis;
}
//如果校验通过并且是MySQL的情况下
if (valid && isMySql) { // unexcepted branch
//获得MySQL服务器中这个会话最后一次执行语句时间
long lastPacketReceivedTimeMs = MySqlUtils.getLastPacketReceivedTimeMs(conn);
//如果这个值>0,即工具类返回正常结果的情况下
if (lastPacketReceivedTimeMs > 0) {
//获得MySQL的空闲时间
long mysqlIdleMillis = currentTimeMillis - lastPacketReceivedTimeMs;
//如果服务端空闲时间>timeBetweenEvictionRunsMillis,则丢弃该连接,此时校验失败
if (lastPacketReceivedTimeMs > 0 //
&& mysqlIdleMillis >= timeBetweenEvictionRunsMillis) {
discardConnection(holder);
String errorMsg = "discard long time none received connection. "
+ ", jdbcUrl : " + jdbcUrl
+ ", version : " + VERSION.getVersionNumber()
+ ", lastPacketReceivedIdleMillis : " + mysqlIdleMillis;
LOG.warn(errorMsg);
return false;
}
}
}
//如果校验成功,并且连接池中其他连接发生过异常
if (valid && onFatalError) {
//加锁并且重置状态
lock.lock();
try {
if (onFatalError) {
onFatalError = false;
}
} finally {
lock.unlock();
}
}
//返回校验结果
return valid;
}
//判断当前连接是否被关闭(DruidPooledConnection内部状态),如果关闭则连接无效
if (conn.isClosed()) {
return false;
}
//如果当前没有配置校验语句,则认为该连接有效
if (null == validationQuery) {
return true;
}
Statement stmt = null;
ResultSet rset = null;
try {
//创建语句对象
stmt = conn.createStatement();
//设置该语句超时响应时间,如果没有配置则不进行操作
if (getValidationQueryTimeout() > 0) {
stmt.setQueryTimeout(validationQueryTimeout);
}
//执行校验语句
rset = stmt.executeQuery(validationQuery);
//如果没有返回结果,则校验失败,连接失效
if (!rset.next()) {
return false;
}
} finally {
//关闭校验时所创建的语句和返回对象
JdbcUtils.close(rset);
JdbcUtils.close(stmt);
}
//判断致命异常状态并重置
if (onFatalError) {
lock.lock();
try {
if (onFatalError) {
onFatalError = false;
}
} finally {
lock.unlock();
}
}
//如果上方分支都不存在问题,则该链接有效
return true;
} catch (Throwable ex) {
// skip
return false;
} finally {
if (sqlFile != null) {
JdbcSqlStat.setContextSqlFile(sqlFile);
}
if (sqlName != null) {
JdbcSqlStat.setContextSqlName(sqlName);
}
}
}
2.1 validConnectionChecker
validConnectionChecker是Druid针对不同数据库所提供不同的检查器,是ValidConnectionChecker
接口的实现类,默认提供MySql
、ORACLE
、SQL_SERVER
、POSTGRESQL
以及OCEANBASE
的检查器,这些检查器中提供了默认的validationQuery
,用于在没有配置validationQuery
的时候进行处理。
这些检查器将在连接池初始化的时候被自动识别加载。
public void init() throws SQLException {
...省略部分代码...
initExceptionSorter();
//调用方法初始化连接检查器
initValidConnectionChecker();
validationQueryCheck();
...省略部分代码...
}
Druid通过驱动类名进行判断,自动识别对应的检查器。
private void initValidConnectionChecker() {
//如果已经设置过检查器,则不继续执行
if (this.validConnectionChecker != null) {
return;
}
//获得当前驱动的类名
String realDriverClassName = driver.getClass().getName();
//对不同类型数据库进行判断并设置对应的检查器
if (JdbcUtils.isMySqlDriver(realDriverClassName)) {
this.validConnectionChecker = new MySqlValidConnectionChecker(usePingMethod);
} else if (realDriverClassName.equals(JdbcConstants.ORACLE_DRIVER)
|| realDriverClassName.equals(JdbcConstants.ORACLE_DRIVER2)) {
this.validConnectionChecker = new OracleValidConnectionChecker();
} else if (realDriverClassName.equals(JdbcConstants.SQL_SERVER_DRIVER)
|| realDriverClassName.equals(JdbcConstants.SQL_SERVER_DRIVER_SQLJDBC4)
|| realDriverClassName.equals(JdbcConstants.SQL_SERVER_DRIVER_JTDS)) {
this.validConnectionChecker = new MSSQLValidConnectionChecker();
} else if (realDriverClassName.equals(JdbcConstants.POSTGRESQL_DRIVER)
|| realDriverClassName.equals(JdbcConstants.ENTERPRISEDB_DRIVER)
|| realDriverClassName.equals(JdbcConstants.POLARDB_DRIVER)) {
this.validConnectionChecker = new PGValidConnectionChecker();
} else if (realDriverClassName.equals(JdbcConstants.OCEANBASE_DRIVER)
|| (realDriverClassName.equals(JdbcConstants.OCEANBASE_DRIVER2))) {
DbType dbType = DbType.of(this.dbTypeName);
this.validConnectionChecker = new OceanBaseValidConnectionChecker(dbType);
}
}
2.2 独特的MySqlValidConnectionChecker
在MySQL检查器中,存在一个特殊的属性usePingMethod
,如果开启了这个属性,MySQL检查器将不会通过执行SQL的方式对连接进行校验,而是会通过MySQL所提供的pingInternal
方法发送ping进行校验,该属性通过连接池中的属性进行传递,默认为false,且暂不支持参数配置,需要自行调用set方法进行设置。
public class MySqlValidConnectionChecker extends ValidConnectionCheckerAdapter implements ValidConnectionChecker, Serializable {
public static final int DEFAULT_VALIDATION_QUERY_TIMEOUT = 1;
public static final String DEFAULT_VALIDATION_QUERY = "SELECT 1";
private static final long serialVersionUID = 1L;
private static final Log LOG = LogFactory.getLog(MySqlValidConnectionChecker.class);
private Class<?> clazz;
private Method ping;
private boolean usePingMethod = false;
public MySqlValidConnectionChecker(boolean usePingMethod){
try {
//通过反射的方式获得pingInternal的实例
clazz = Utils.loadClass("com.mysql.jdbc.MySQLConnection");
if (clazz == null) {
clazz = Utils.loadClass("com.mysql.cj.jdbc.ConnectionImpl");
}
if (clazz != null) {
ping = clazz.getMethod("pingInternal", boolean.class, int.class);
}
//如果没有加载成功的话,则依旧会使用SQL进行校验
if (ping != null && usePingMethod == true) {
this.usePingMethod = true;
}
} catch (Exception e) {
LOG.warn("Cannot resolve com.mysql.jdbc.Connection.ping method. Will use 'SELECT 1' instead.", e);
}
configFromProperties(System.getProperties());
}
public boolean isValidConnection(Connection conn, String validateQuery, int validationQueryTimeout) throws Exception {
if (conn.isClosed()) {
return false;
}
//判断是否使用ping进行校验
if (usePingMethod) {
if (conn instanceof DruidPooledConnection) {
conn = ((DruidPooledConnection) conn).getConnection();
}
if (conn instanceof ConnectionProxy) {
conn = ((ConnectionProxy) conn).getRawObject();
}
//判断连接对象和连接对象的关系是否可以转化(是一样的类/接口或超类/超接口)
if (clazz.isAssignableFrom(conn.getClass())) {
if (validationQueryTimeout <= 0) {
validationQueryTimeout = DEFAULT_VALIDATION_QUERY_TIMEOUT;
}
try {
//执行方法进行校验
ping.invoke(conn, true, validationQueryTimeout * 1000);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof SQLException) {
throw (SQLException) cause;
}
throw e;
}
return true;
}
}
String query = validateQuery;
if (validateQuery == null || validateQuery.isEmpty()) {
query = DEFAULT_VALIDATION_QUERY;
}
Statement stmt = null;
ResultSet rs = null;
try {
stmt = conn.createStatement();
if (validationQueryTimeout > 0) {
stmt.setQueryTimeout(validationQueryTimeout);
}
rs = stmt.executeQuery(query);
return true;
} finally {
JdbcUtils.close(rs);
JdbcUtils.close(stmt);
}
}
}
3. testWhileIdle的校验时点
testOnBorrow、testOnReturn两者分别在获得连接/归还连接时进行校验,无特殊内容。
如果以字面意思来看,将会有一个线程或定时器判断链接是否空闲一段时间(timeBetweenEvictionRunsMillis),实际这个时点却是在获得链接时进行的判断。
由于testOnBorrow和testWhileIdle都是检查连接有效性,testOnBorrow是每次获取连接必定检查,testWhileIdle是超过一定时间才检查,所以当开启testOnBorrow时,testWhileIdle将不会执行。
public DruidPooledConnection getConnectionDirect(long maxWaitMillis) throws SQLException {
int notFullTimeoutRetryCnt = 0;
for (;;) {
// handle notFullTimeoutRetry
DruidPooledConnection poolableConnection;
try {
poolableConnection = getConnectionInternal(maxWaitMillis);
} catch (GetConnectionTimeoutException ex) {
if (notFullTimeoutRetryCnt <= this.notFullTimeoutRetryCount && !isFull()) {
notFullTimeoutRetryCnt++;
if (LOG.isWarnEnabled()) {
LOG.warn("get connection timeout retry : " + notFullTimeoutRetryCnt);
}
continue;
}
throw ex;
}
//如果开启了testOnBorrow,则每次获得连接时都执行一次校验
if (testOnBorrow) {
boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
if (!validate) {
if (LOG.isDebugEnabled()) {
LOG.debug("skip not validate connection.");
}
discardConnection(poolableConnection.holder);
continue;
}
} else {
//判断连接是否被关闭,如果被关闭,则销毁当前连接(此时Holder应该为空)
if (poolableConnection.conn.isClosed()) {
discardConnection(poolableConnection.holder); // 传入null,避免重复关闭
continue;
}
//如果开启了testWhileIdle,判断当前连接是空闲了所配置的时间,如果没有则不进行校验
if (testWhileIdle) {
//得到当前连接的持有者,便于获得统计信息
final DruidConnectionHolder holder = poolableConnection.holder;
long currentTimeMillis = System.currentTimeMillis();
//得到当前连接上次的活跃时间、上次的执行时间、上次校验存活的时间(归还连接时)
long lastActiveTimeMillis = holder.lastActiveTimeMillis;
long lastExecTimeMillis = holder.lastExecTimeMillis;
long lastKeepTimeMillis = holder.lastKeepTimeMillis;
//如果开启了checkExecuteTime参数,并且最后执行时间与最后活跃时间不一致,则认为最后的活跃时间是执行时间
if (checkExecuteTime
&& lastExecTimeMillis != lastActiveTimeMillis) {
lastActiveTimeMillis = lastExecTimeMillis;
}
//最后校验存活时间大于上次活跃时间,则取最后校验存活时间
if (lastKeepTimeMillis > lastActiveTimeMillis) {
lastActiveTimeMillis = lastKeepTimeMillis;
}
//空闲时间=当前时间-上次活跃时间
long idleMillis = currentTimeMillis - lastActiveTimeMillis;
//获得timeBetweenEvictionRunsMillis参数
long timeBetweenEvictionRunsMillis = this.timeBetweenEvictionRunsMillis;
//如果没有配置这个参数,取默认值,60000ms,即1分钟
if (timeBetweenEvictionRunsMillis <= 0) {
timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
}
//如果空闲时间超过了timeBetweenEvictionRunsMillis或者空闲时间小于0(此时可能发生时钟回拨等问题?)
if (idleMillis >= timeBetweenEvictionRunsMillis
|| idleMillis < 0 // unexcepted branch
) {
//校验链接状态
boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
if (!validate) {
if (LOG.isDebugEnabled()) {
LOG.debug("skip not validate connection.");
}
discardConnection(poolableConnection.holder);
continue;
}
}
}
}
if (removeAbandoned) {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
poolableConnection.connectStackTrace = stackTrace;
poolableConnection.setConnectedTimeNano();
poolableConnection.traceEnable = true;
activeConnectionLock.lock();
try {
activeConnections.put(poolableConnection, PRESENT);
} finally {
activeConnectionLock.unlock();
}
}
if (!this.defaultAutoCommit) {
poolableConnection.setAutoCommit(false);
}
return poolableConnection;
}
}