DruidDataSource#getConnection
@Override
public DruidPooledConnection getConnection() throws SQLException {
return getConnection(maxWait);
}
public DruidPooledConnection getConnection(long maxWaitMillis) throws SQLException {
//初始化 所以init方法是第一次获取连接的时候才会初始化
init();
if (filters.size() > 0) {
// 如果有拦截器 拦截器链 责任链模式执行所有的拦截
FilterChainImpl filterChain = new FilterChainImpl(this);
// 拦截器获取数据库连接代码详解 , 应该也是执行完拦截后获取连接
return filterChain.dataSource_connect(this, maxWaitMillis);
} else {
// 直接获取连接
return getConnectionDirect(maxWaitMillis);
}
}
//直接获取连接
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;
}
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;
}
// todo 不太能理解 既然已经在init的时候已经启动了守护线程createAndStartDestroyThread去进行空闲线程和超时线程的定时扫描和回收,为什么在获取连接的时候还要重新处理一次呢???
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中找到相关代码,查看报错相关的变量的逻辑
if (validConnectionChecker != null) {
//校验连接状态,其中的validationQuery就是我们要执行的sql语句,validationQueryTimeOut表示执行sql的超时时间,valid表示是否检验成功或者正常响应
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
//获取最近一次查询数据库接受数据包的时间
long lastPacketReceivedTimeMs = MySqlUtils.getLastPacketReceivedTimeMs(conn);
if (lastPacketReceivedTimeMs > 0) {
//当前时间与最后一次接受数据的时间差
long mysqlIdleMillis = currentTimeMillis - lastPacketReceivedTimeMs;
//如果时间差大于druid的配置timeBetweenEvictionRunsMislis就开始报错。
//timeBetweenEvictionRunsMislis的默认时间是60秒
if (lastPacketReceivedTimeMs > 0
&& mysqlIdleMillis >= timeBetweenEvictionRunsMillis) {
discardConnection(holder);
String errorMsg = "discard long time none received connection. "
+ ", jdbcUrl : " + jdbcUrl
+ ", jdbcUrl : " + jdbcUrl
+ ", lastPacketReceivedIdleMillis : " + mysqlIdleMillis;
LOG.error(errorMsg);
return false;
}
}
}
通过上述分析,我们要立马就知道,我们要想不报错就需要将timeBetweenEvictionRunsMislis的时间变大。于是我将这个值改大了再试试,结果还是在报错。
主要是lastPacketReceivedTimeMs 代表的是最后一次接受数据包的时间。显然这里的时间可以是昨天或者很久以前。那么为了控制让程序不要执行这里的报错和返回false,只能通过修改这里的valid了。我们看一下这里的检验连接可用性的代码:
public boolean isValidConnection(Connection conn, String validateQuery, int
validationQueryTimeout) throws Exception {
if (conn.isClosed()) {
return false;
}
//判断usePingMethod是否为true
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;
}
//使用该类中的ping类进行执行。这里的ping并不是执行sql,而是ping命令
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;
}
}
//如果usePingMethod为false,那么就执行sql
String query = validateQuery;
//这里的validateQuery,就是spring.datasource.druid.validation-query的配置项,这里如果为空的话,就采用select 1
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);
}
}
}
那么这里的usePingMethod又是如何判断的,按理说这里的值应该是true,然后导致后边的检索库的sql没有进行。也就导致获取最近的接受mysql数据包的时间没有更新。那么问题的核心又变成了usePingMethod的赋值问题。发现在初始化的时候就就行了usePingMethod的初始化
public MySqlValidConnectionChecker(){
try {
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);
}
if (ping != null) {
usePingMethod = true;
}
} catch (Exception e) {
LOG.warn("Cannot resolve com.mysql.jdbc.Connection.ping method. Will use 'SELECT 1' instead.", e);
}
configFromProperties(System.getProperties());
}
@Override
public void configFromProperties(Properties properties) {
String property = properties.getProperty("druid.mysql.usePingMethod");
if ("true".equals(property)) {
setUsePingMethod(true);
} else if ("false".equals(property)) {
setUsePingMethod(false);
}
}
通过上述分析,我们大概明白了错误的原因,那么我们需要明白这个错误导致返回false,最后是否会对业务有什么影响。我们需要知道是谁在调这块的代码
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;
}
if (testOnBorrow) {
//测试数据库可用性
boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
if (!validate) {
if (LOG.isDebugEnabled()) {
LOG.debug("skip not validate connection.");
}
discardConnection(poolableConnection.holder);
continue;
}
}
当testConnectionInternal返回为false的时候,就会执行这里的discardConnection方法,这里要释放这个连接了,
public void discardConnection(DruidConnectionHolder holder) {
if (holder != null) {
Connection conn = holder.getConnection();
if (conn != null) {
//这块应该是释放的主要动作了。
JdbcUtils.close(conn);
}
//多线程加锁
this.lock.lock();
try {
if (holder.discard) {
return;
}
if (holder.active) {
//让活动的连接数减少一个
--this.activeCount;
holder.active = false;
}
//销毁的个数+1
++this.discardCount;
holder.discard = true;
//如果活动的线程小于最小的连接数
if (this.activeCount <= this.minIdle) {
//这里释放信号量,负责创建的线程会新建连接
this.emptySignal();
}
} finally {
this.lock.unlock();
}
}
JdbcUtils.close(conn);主要设置了一下判断标志
public static void close(Connection x) {
if (x != null) {
try {
if (x.isClosed()) {
return;
}
x.close();
} catch (Exception var2) {
LOG.debug("close connection error", var2);
}
}
}
通过上述代码,我们并没有发现销毁数组中连接的操作,那么我们看创建新链接的的时候有没有相关的逻辑,方法如下:
private void emptySignal() {
if (this.createScheduler == null) {
this.empty.signal();
} else if (this.createTaskCount < this.maxCreateTaskCount) {
//如果创建的连接小于最大的连接
if (this.activeCount + this.poolingCount + this.createTaskCount < this.maxActive) {
//正在活动的连接+连接池中剩下的+现在要创建的之<允许的最大的连接;
// 就创建一个
this.submitCreateTask(false);
}
}
}
druid connections[]数组
DruidConnectionHolder takeLast() throws InterruptedException, SQLException {
try {
while(this.poolingCount == 0) {
//如果连接池中连接数为0,就发信号,让其创建的线程来创建
this.emptySignal();
if (this.failFast && this.isFailContinuous()) {
throw new DataSourceNotAvailableException(this.createError);
}
++this.notEmptyWaitThreadCount;
if (this.notEmptyWaitThreadCount > this.notEmptyWaitThreadPeak) {
this.notEmptyWaitThreadPeak = this.notEmptyWaitThreadCount;
}
try {
//如果池子中为空,就阻塞。等待创建线程创建好了之后进行唤醒
this.notEmpty.await();
} finally {
--this.notEmptyWaitThreadCount;
}
++this.notEmptyWaitCount;
if (!this.enable) {
connectErrorCountUpdater.incrementAndGet(this);
if (this.disableException != null) {
throw this.disableException;
}
throw new DataSourceDisableException();
}
}
} catch (InterruptedException var5) {
this.notEmpty.signal();
++this.notEmptySignalCount;
throw var5;
}
//将连接池中的连接减少一个,因为这个要出去干活了。
this.decrementPoolingCount();
//拿到这个连接
DruidConnectionHolder last = this.connections[this.poolingCount];
//设置为空,gc的时候进行空间释放
this.connections[this.poolingCount] = null;
//返回
return last;
通过上述分析,我们知道获取连接的时候是通过connects数组获取的,获取之后就交给业务。所以说上边的释放对连接池没有任何影响,所以对业务没有影响。通过上述分析,我们对druid数据库连接池的工作过程有了很近一步的理解。至于上述的连接置为null的操作在线程池中也是相同的做法
对于druid连接池来说。报错信息:
([com.alibaba.druid.pool.DruidAbstractDataSource:]) discard long time none received connection.
不会影响业务。避免报错的方法是在项目启动的时候通过脚本添加是否使用ping命令来检测连接的可用性。druid读取该配置的时候直接读取的系统变量。所以在项目中添加配置是没有作用的。当上述报错产生之后,druid会将连接销毁,并尝试从连接池中获取新链接。如果没有的话就会创建。其中的界限条件就是最小连接数,最大连接数等。除此之外,druid有两个线程,分别为连接创建线程和心跳检测线程。他们相互配合保证连接的可用性和连接异步创建,所以对业务来说,总是有连接可用的