在druid初始化的过程中,会创建一个线程销毁任务DestroyTask,该任务将会以一定的频率执行,默认是1s执行一次,也可以通过timeBetweenEvictionRunsMillis设置多长时间执行一次。下面重点看一下DestroyTask做了哪些事情:
public class DestroyTask implements Runnable {
public void run() {
shrink(true, keepAlive);
//如果removeAbandoned=true,则检查活动连接,
//或者说是检查正在被应用线程使用的连接,
//如果看过前面几篇文章,应该还记得从连接池获取连接时,如果removeAbandoned=true,那么连接会被放入一个Map对象中,removeAbandoned()就是遍历这个Map对象,
//如果连接的使用时间大于了removeAbandonedTimeoutMillis,
//这样的连接就会在removeAbandoned()里面被关闭
//所以可以使用这个功能监控连接泄漏
if (isRemoveAbandoned()) {
removeAbandoned();
}
}
}
shrink从字面意思理解是收缩,所以我们可以猜测该方法的作用是减少连接池中的连接,下面解析一下shrink()方法:
public void shrink(boolean checkTime, boolean keepAlive) {
boolean needFill = false;
int evictCount = 0;
int keepAliveCount = 0;
//fatalErrorCount记录了连接出现致命错误的总次数
//fatalErrorCountLastShrink记录了最后一次调用shrink()方法时连接出现致命错误的总次数
int fatalErrorIncrement = fatalErrorCount - fatalErrorCountLastShrink;
fatalErrorCountLastShrink = fatalErrorCount;
try {
final int checkCount = poolingCount - minIdle;
final long currentTimeMillis = System.currentTimeMillis();
//遍历线程池中的所有连接
for (int i = 0; i < poolingCount; ++i) {
DruidConnectionHolder connection = connections[i];
//如果连接在最后一次使用过程中出现过致命错误(是否致命是通过ExceptionSorter判断的),
//并且fatalErrorIncrement > 0(表示在上次调用shrink()方法后)或者onFatalError=true(表示fatalErrorIncrement大于了设定的最大值)
//那么会将连接放入到keepAliveConnections数组中,该方法后面会对该数组进一步处理
if ((onFatalError || fatalErrorIncrement > 0) && (lastFatalErrorTimeMillis > connection.connectTimeMillis)) {
keepAliveConnections[keepAliveCount++] = connection;
continue;
}
if (checkTime) {
if (phyTimeoutMillis > 0) {
long phyConnectTimeMillis = currentTimeMillis - connection.connectTimeMillis;
//如果连接的使用时间超过了最大允许使用时间,那么将连接放入到evictConnections数组中
if (phyConnectTimeMillis > phyTimeoutMillis) {
evictConnections[evictCount++] = connection;
continue;
}
}
//idleMillis表示连接在池中的生存时间,其实idleMillis与phyConnectTimeMillis相等
long idleMillis = currentTimeMillis - connection.lastActiveTimeMillis;
//下面的if条件表示连接在池中的生存时间小于最小存活时间
// 而且还小于keepAliveBetweenTimeMillis(表示需要保持存活的检查时间间隔,也就是检查连接是否继续存活的最小时间间隔)
if (idleMillis < minEvictableIdleTimeMillis
&& idleMillis < keepAliveBetweenTimeMillis
) {
//通过前面的文章介绍,大家应该了解连接池中的连接下标越大则connection.lastActiveTimeMillis越大,
//所以如果当前连接的idleMillis符合了if条件,那么它后面的连接肯定也符合if条件,
//所以这里使用了break,因为后面的连接没有遍历的必要了
break;
}
//minEvictableIdleTimeMillis表示连接在池中最小生存时间
//如果连接在池中的生存时间超过了最大存活时间或者没有达到本次最少遍历连接数,
// 则将连接放入evictConnections数组中
if (idleMillis >= minEvictableIdleTimeMillis) {
if (checkTime && i < checkCount) {
evictConnections[evictCount++] = connection;
continue;
} else if (idleMillis > maxEvictableIdleTimeMillis) {
evictConnections[evictCount++] = connection;
continue;
}
}
//如果keepAlive=true,并且idleMillis >= keepAliveBetweenTimeMillis,
// 则将连接放入keepAliveConnections数组
if (keepAlive && idleMillis >= keepAliveBetweenTimeMillis) {
keepAliveConnections[keepAliveCount++] = connection;
}
} else {
if (i < checkCount) {
evictConnections[evictCount++] = connection;
} else {
break;
}
}
}
int removeCount = evictCount + keepAliveCount;
if (removeCount > 0) {
//将连接池中最前面的removeCount个连接删除,并且将剩余的poolingCount - removeCount个连接放到连接池的最前面
System.arraycopy(connections, removeCount, connections, 0, poolingCount - removeCount);
Arrays.fill(connections, poolingCount - removeCount, poolingCount, null);
poolingCount -= removeCount;//相应的可用连接数减少
}
keepAliveCheckCount += keepAliveCount;
//如果当前的所有连接小于minIdle,则需要再创建连接
if (keepAlive && poolingCount + activeCount < minIdle) {
needFill = true;
}
} finally {
lock.unlock();
}
if (evictCount > 0) {
//遍历evictConnections数组,将连接关闭
for (int i = 0; i < evictCount; ++i) {
DruidConnectionHolder item = evictConnections[i];
Connection connection = item.getConnection();
JdbcUtils.close(connection);
destroyCountUpdater.incrementAndGet(this);
}
Arrays.fill(evictConnections, null);//清空数组
}
if (keepAliveCount > 0) {
// keep order
//遍历可以存活的连接
for (int i = keepAliveCount - 1; i >= 0; --i) {
DruidConnectionHolder holer = keepAliveConnections[i];
Connection connection = holer.getConnection();
holer.incrementKeepAliveCheckCount();
boolean validate = false;
try {
//首先对连接进行校验
this.validateConnection(connection);
validate = true;
} catch (Throwable error) {
if (LOG.isDebugEnabled()) {
LOG.debug("keepAliveErr", error);
}
// skip
}
boolean discard = !validate;
if (validate) {
//连接校验通过,则将连接再次放入连接池中
holer.lastKeepTimeMillis = System.currentTimeMillis();
boolean putOk = put(holer, 0L, true);
if (!putOk) {
discard = true;
}
}
if (discard) {
try {
connection.close();
} catch (Exception e) {
// skip
}
lock.lock();
try {
discardCount++;
//如果连接数过少,则发出信号,让连接创建线程开始创建连接
if (activeCount + poolingCount <= minIdle) {
emptySignal();
}
} finally {
lock.unlock();
}
}
}
this.getDataSourceStat().addKeepAliveCheckCount(keepAliveCount);
Arrays.fill(keepAliveConnections, null);
}
//根据前面的设置,如果连接数少于了minIdle,那么需要补充连接
if (needFill) {
lock.lock();
try {
//createTaskCount表示后台线程创建连接的个数
//fillCount表示需要补充的连接数
int fillCount = minIdle - (activeCount + poolingCount + createTaskCount);
for (int i = 0; i < fillCount; ++i) {
emptySignal();
}
} finally {
lock.unlock();
}
} else if (onFatalError || fatalErrorIncrement > 0) {
//如果根据判断不需要立马新增连接,但是连接之前发生过致命错误或者致命错误大于了设定的最大值,那么也发出信号补充连接
lock.lock();
try {
emptySignal();
} finally {
lock.unlock();
}
}
}
shrink()方法代码比较多,而且也是非常重要的方法,下面我们再梳理一下该方法。
shrink()方法里面涉及到了两个数组evictConnections和keepAliveConnections:
- evictConnections:如果连接的使用时间超过了最大允许使用时间,或者连接在池中的生存时间超过了最大允许存活时间或者没有达到本次最少遍历连接数(最少遍历连接数=poolingCount - minIdle),则将连接放入该数组中,放入该数组的连接都是接下来要清理的连接;
- keepAliveConnections:该数组中的连接如果能够通过validateConnection()校验,那么这些连接还可以放入连接池,否则直接从连接池中清除,当连接符合以下条件之一时,会被放入该数组中:1)当前连接在最后一次使用过程中出现过致命错误(是否致命是通过ExceptionSorter判断的)并且fatalErrorIncrement > 0(fatalErrorIncrement 表示在上次调用shrink()方法后所有连接出现致命错误的个数)或者onFatalError=true(表示fatalErrorIncrement大于了设定的最大值);2)如果keepAlive=true,并且当前连接在池中的存活时间>= 检查连接是否继续存活的最小时间间隔。
shrink()方法从连接池的头开始遍历,将连接分别放入evictConnections和keepAliveConnections数组中,并且将连接从连接池中清除,如果不符合两个数组的条件,那么连接继续放在连接池中,接下来遍历keepAliveConnections数组,因为该数组的连接可能可以继续存活,所以对连接调用validateConnection()校验,如果校验通过,那么将连接放入到连接池中,否则关闭连接并清理掉,最后检查连接数包括连接池中的连接和应用线程正在使用的连接,如果总数小于minIdle,那么发出信号,让连接创建后台线程开始创建连接。
这样我们就可以知道DestroyTask的作用了:
- 检查连接池中的连接,将不符合要求的连接清理掉,保留最新创建的连接;
- 如果removeAbandoned=true,则检查连接使用时间是否超过了最大值,如果超过,则关闭连接。
本文中涉及到了几个重要的属性,下面再汇总下:
- minEvictableIdleTimeMillis:连接在池中的最小存活时间;
- keepAliveBetweenTimeMillis:检查连接是否继续存活的最小时间间隔,当连接的存活时间小于keepAliveBetweenTimeMillis,则连接继续放在连接池中,不做任何处理,如果大于等于了keepAliveBetweenTimeMillis且keepAlive=true,那么就会对连接进行检查;
- maxEvictableIdleTimeMillis:连接在池中的最大存活时间,如果连接超过了该值,那么该连接必须关闭;
- activeCount:应用线程正在使用的连接数;
- poolingCount:连接池中的总连接数,不包括activeCount
- createTaskCount:正在创建连接的后台线程数;
- removeAbandoned:是否开启连接泄露检测诊断,默认为false;
- removeAbandonedTimeout:单位毫秒,当连接超过removeAbandonedTimeout未关闭,则会被强行回收。
连接泄漏检测资料:https://github.com/alibaba/druid/wiki/%E8%BF%9E%E6%8E%A5%E6%B3%84%E6%BC%8F%E7%9B%91%E6%B5%8B