但凡涉及到数据库的应用,基本上都离不开数据库连接池工具的使用,但在使用时,对于应用而言,无需关注你获取到的连接是否可用,不像之前,有个著名的MySQL连接8小时问题,现在根本不用关心从连接池拿到的连接是否可用,你拿到了说明就是可用的,这一点是数据库连接池的框架保证的,常见的数据库连接池都有保证,今天主要探讨下SpringBoot默认的数据库连接池HikariCP和Durid连接池在连接的有效性探测的差异。
从配置上来说,两者都提供了通过sql来检查活性。
Druid的不分配置如下:
validationQuery=select 'x'
testWhileIdle=true
testOnBorrow=true
testOnReturn=true
HikariCP的部分配置如下:
connectionTestQuery=select 'x'
从以上部分配置看出,两个都提供了相关配置,以保证获取的连接是有效的。
我们先来看HikariCP的在获取连接时的有效性检查,HikariCP获取连接时的有效性检查时必须的,没有提供不需要检查的相关配置。HikariCP的连接获取是从HikariPool中获取的,部分源码实现如下:
public final class HikariPool extends PoolBase implements HikariPoolMXBean, IBagStateListener {.../*** Get a connection from the pool, or timeout after connectionTimeout milliseconds.** @return a java.sql.Connection instance* @throws SQLException thrown if a timeout occurs trying to obtain a connection*/public Connection getConnection() throws SQLException{return getConnection(connectionTimeout);}/*** Get a connection from the pool, or timeout after the specified number of milliseconds.** @param hardTimeout the maximum time to wait for a connection from the pool* @return a java.sql.Connection instance* @throws SQLException thrown if a timeout occurs trying to obtain a connection*/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();// 通过isConnectionAlive判断连接的活性,继承自父类PoolBaseif (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();}}...
} abstract class PoolBase {PoolBase(final HikariConfig config){this.config = config;this.networkTimeout = UNINITIALIZED;this.catalog = config.getCatalog();this.schema = config.getSchema();this.isReadOnly = config.isReadOnly();this.isAutoCommit = config.isAutoCommit();this.exceptionOverride = UtilityElf.createInstance(config.getExceptionOverrideClassName(), SQLExceptionOverride.class);this.transactionIsolation = UtilityElf.getTransactionIsolation(config.getTransactionIsolation());this.isQueryTimeoutSupported = UNINITIALIZED;this.isNetworkTimeoutSupported = UNINITIALIZED;// 这里判断使用那种方式进行连接的探活this.isUseJdbc4Validation = config.getConnectionTestQuery() == null;this.isIsolateInternalQueries = config.isIsolateInternalQueries();this.poolName = config.getPoolName();this.connectionTimeout = config.getConnectionTimeout();this.validationTimeout = config.getValidationTimeout();this.lastConnectionFailure = new AtomicReference<>();initializeDataSource();}boolean isConnectionAlive(final Connection connection){try {try {setNetworkTimeout(connection, validationTimeout);final int validationSeconds = (int) Math.max(1000L, validationTimeout) / 1000;// 判断是否使用Jdbc4的方式进行探测if (isUseJdbc4Validation) {return connection.isValid(validationSeconds);}// 使用探测语句的方式探活try (Statement statement = connection.createStatement()) {if (isNetworkTimeoutSupported != TRUE) {setQueryTimeout(statement, validationSeconds);}statement.execute(config.getConnectionTestQuery());}}finally {setNetworkTimeout(connection, networkTimeout);if (isIsolateInternalQueries && !isAutoCommit) {connection.rollback();}}return true;}catch (Exception e) {lastConnectionFailure.set(e);logger.warn("{} - Failed to validate connection {} ({}). Possibly consider using a shorter maxLifetime value.",poolName, connection, e.getMessage());return false;}}
}
通过以上部分源码我们基本上可以知道,HikariCP有两种方式判断连接的有效性,一种是配置探测SQL的方式,另一种是UseJdbc4Validation
的方式。在PoolBase的初始化片段中,是否使用Jdbc4的探活和应用是否配置了探测SQL有关:this.isUseJdbc4Validation = config.getConnectionTestQuery() == null;
,如果没有配置探活SQL,那么就使用Jdbc4的探活方式,所以,HikariCP默认使用的探活方式就是Jdbc4的方式。
通过配置部分可知,Druid的配置参数相对多一些,其中探活只能配置sql,另外两个参数testWhileIdle和testOnBorrow两个参数分别控制在连接空闲时是否探测有效性以及从连接池中获取连接池是否需要有效性探测,部分源码如下:
public class DruidDataSource extends DruidAbstractDataSource implements DruidDataSourceMBean, ManagedDataSource, Referenceable, Closeable, Cloneable, ConnectionPoolDataSource, MBeanRegistration {public DruidPooledConnection getConnectionDirect(long maxWaitMillis) throws SQLException {int notFullTimeoutRetryCnt = 0;for (;;) {// handle notFullTimeoutRetryDruidPooledConnection 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;}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 {if (poolableConnection.conn.isClosed()) {discardConnection(poolableConnection.holder); // 传入null,避免重复关闭continue;}if (testWhileIdle) {final DruidConnectionHolder holder = poolableConnection.holder;long currentTimeMillis = System.currentTimeMillis();long lastActiveTimeMillis = holder.lastActiveTimeMillis;long lastExecTimeMillis = holder.lastExecTimeMillis;long lastKeepTimeMillis = holder.lastKeepTimeMillis;if (checkExecuteTime&& lastExecTimeMillis != lastActiveTimeMillis) {lastActiveTimeMillis = lastExecTimeMillis;}if (lastKeepTimeMillis > lastActiveTimeMillis) {lastActiveTimeMillis = lastKeepTimeMillis;}long idleMillis = currentTimeMillis - lastActiveTimeMillis;long timeBetweenEvictionRunsMillis = this.timeBetweenEvictionRunsMillis;if (timeBetweenEvictionRunsMillis <= 0) {timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;}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;}}
}
从以上源码可知,Druid从连接池获取连接时先通过参数testOnBorrow判断是否需要检测,其内部的检测是通过配置的validationQuery
进行查询检查的,并没有发现Jdbc4的方式进行有效性探测。
JDBC4的isValid方法来测试连接是否可用,是通过向数据库服务器发送一个ping请求来实现的。这个ping请求的实现方式可能因数据库厂商而异,但通常包括向数据库服务器发送一个简单的网络数据包,以测试连接是否正常。
相比之下,使用isValid方法测试连接是否可用的优点是:
JDBC4的isValid方法的原理是基于底层网络连接的有效性进行检测,它使用了底层协议的心跳机制来检测连接的有效性。当调用isValid方法时,JDBC驱动程序会发送一个心跳包到数据库服务器,等待数据库服务器的响应。如果在指定的超时时间内收到了响应,则认为连接是有效的,否则认为连接已经失效。
总之,使用isValid方法测试连接是否可用,比传统方法更快速、更简单、更可靠、更兼容和更可移植。因此,建议使用isValid方法来测试连接是否可用。