接上一篇文章,研究Druid连接池的连接回收线程DestroyThread,通过调用destroyTask.run->DruidDataSourcek.shrink完成过期连接的回收。
DruidDataSourcek.shrink
理解DruidDataSourcek的连接回收方法shrink有一个必要前提:Druid的getConnection方法总是从connectoins的尾部获取连接,所以闲置连接最有可能出现在connections数组的头部,闲置超期需要被回收的连接也应该处于connections的头部(数组下标较小的对象)。
在这个基础上,我们开始分析代码。
public void shrink(boolean checkTime, boolean keepAlive) {
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
return;
}
boolean needFill = false;
int evictCount = 0;
int keepAliveCount = 0;
int fatalErrorIncrement = fatalErrorCount - fatalErrorCountLastShrink;
fatalErrorCountLastShrink = fatalErrorCount;
首先获取锁资源,并初始化控制变量。
try {
if (!inited) {
return;
}
final int checkCount = poolingCount - minIdle;
final long currentTimeMillis = System.currentTimeMillis();
for (int i = 0; i < poolingCount; ++i) {
DruidConnectionHolder connection = connections[i];
if ((onFatalError || fatalErrorIncrement > 0) && (lastFatalErrorTimeMillis > connection.connectTimeMillis)) {
keepAliveConnections[keepAliveCount++] = connection;
continue;
}
如果初始化尚未完成,则不能开始做清理动作,直接返回。
计算checkCount,checkCount的意思是本次需要清理、或者需要检查的连接数量,checkCount等于连接池数量减去参数设置的需要保持的最小空闲连接数。很好理解,清理完成之后仍然需要确保最小空闲连接数。
之后循环逐个检查连接池connections中的所有连接,从头部(connections[0])开始。
如果清理过程中发生了错误,并且错误发生的时间是在当前连接的连接获取时间之后,则将当前连接放入keepAliveConnections中,继续检查下一个连接。
然后:
if (checkTime) {
if (phyTimeoutMillis > 0) {
long phyConnectTimeMillis = currentTimeMillis - connection.connectTimeMillis;
if (phyConnectTimeMillis > phyTimeoutMillis) {
evictConnections[evictCount++] = connection;
continue;
}
}
long idleMillis = currentTimeMillis - connection.lastActiveTimeMillis;
if (idleMillis < minEvictableIdleTimeMillis
&& idleMillis < keepAliveBetweenTimeMillis
) {
break;
}
if (idleMillis >= minEvictableIdleTimeMillis) {
if (checkTime && i < checkCount) {
evictConnections[evictCount++] = connection;
continue;
} else if (idleMillis > maxEvictableIdleTimeMillis) {
evictConnections[evictCount++] = connection;
continue;
}
}
if (keepAlive && idleMillis >= keepAliveBetweenTimeMillis) {
keepAliveConnections[keepAliveCount++] = connection;
}
}
这段代码对应的条件是checkTime,checkTime的意思是:是否检查数据库连接的空闲时间,是调用shrink方法时传入的,destoryThread任务调用shrink方法时传入的是true,所以会走到这段代码逻辑中。
如果参数设置的phyTimeoutMillis,即物理连接的超时时间>0的话,则检查当前连接创建以来到现在的时长如果已超时的话,当前连接放入evictConnections中,准备回收。
然后计算当前连接的空闲时间idleMillis,如果空闲时间小于参数设置的连接最小空闲回收时间minEvictableIdleTimeMillis,并且也小于保持存活时间keepAliveBetweenTimeMillis,则结束当前循环,不再检查连接池中剩余连接。
这里的逻辑其实也是基于connections的特性:数组第一个元素空闲时间最长,从左到右的空闲时间越来越短,如果从左到右检查过程中发现当前元素空闲时间没有达到需要回收的时长的话,就没必要检查连接池中后续的元素了。
否则如果当前连接空闲时长idleMillis大于等于minEvictableIdleTimeMillis的话,则判断checkTime && i < checkCount的话则将当前连接放入evictConnections中准备回收。此处i < checkCount的意思就是,回收后的连接数量仍然能够确保最小空闲连接数的要求,则直接回收当前连接。
否则,就是i>=checkCount情况,这种情况下如果发生回收的话,必然会导致连接池中的剩余连接数不能满足参数设置的最小空闲连接数的要求、必须要重新创建连接了。但是如果空闲时长大于maxEvictableIdleTimeMillis,也必须是要回收的,所以,将当前连接放入evictConnections准备回收。
有关连接回收,多说一句,连接池参数maxEvictableIdleTimeMillis一般会根据数据库端的参数进行配置,连接闲置超过一定时长的话,数据库会主动关闭连接,这种情况下即使应用端连接池不关闭连接,该连接也不可用了。所以为了确保连接可用,一般情况下应用端数据库连接池的maxEvictableIdleTimeMillis应该设置为小于数据库端的最大空闲时长。
然后判断如果keepAlive(参数设置,默认false)并且当前连接的空闲时间idleMillis大于等于参数设置的保活时长keepAliveBetweenTimeMillis的话,则当前连接放入keepAliveConnections中保活。
接下来:
} else {
if (i < checkCount) {
evictConnections[evictCount++] = connection;
} else {
break;
}
}
}
就是checkTime=false的情况,意思就是不检查空闲时长,那么能够确保最小空闲连接数的前提下,其他连接都可以回收,所以要把connections中小于checkCount((i < checkCount)的连接全部放入evictConnections中回收。
连接池中的连接检查完毕,该回收连接放在evictConnections中,该保活的放在keepAliveConnections中。接下来的代码开始真正处理回收和保活。
首先清理连接池connections:
int removeCount = evictCount + keepAliveCount;
if (removeCount > 0) {
System.arraycopy(connections, removeCount, connections, 0, poolingCount - removeCount);
Arrays.fill(connections, poolingCount - removeCount, poolingCount, null);
poolingCount -= removeCount;
}
计算需要移除的连接数量removeCount等于回收数量与保活数量之和,然后将connections中的位于removeCount之后的元素前移,使其处于connnections数组的头部,并重新计算poolingCount。
接下来计算保活数量:
keepAliveCheckCount += keepAliveCount;
if (keepAlive && poolingCount + activeCount < minIdle) {
needFill = true;
}
} finally {
lock.unlock();
}
累加keepAliveCheckCount,并且判断如果连接池数量小于最小空闲数的话,设置needFill为true。
释放锁资源。
然后回收连接:
if (evictCount > 0) {
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);
}
将evictConnections中的连接逐个回收:关闭连接,并累加destroyCount,并重新初始化evictConnections。
接下来处理保活连接:
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);
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);
}
逐个处理keepAliveConnections中的连接。
调用validateConnection方法检查当前连接是否可用,如果连接仍然可用,则更新连接的lastKeepTimeMillis为当前系统时间后,调用put方法将连接重新放回连接池connections中。如果放回失败则关闭连接。连接关闭后检查当前连接池数量activeCount + poolingCount <= minIdle则调用emptySignal();创建连接。
之后将keepAliveCount加入到连接统计分析数据中,重置keepAliveConnections数组。
最后:
if (needFill) {
lock.lock();
try {
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();
}
}
}
检查如果needFill,说明当前连接池数量不能满足参数设置的最小空闲连接数,则获取锁资源,计算需要创建的连接数,调用emptySignal();创建连接填充连接池直到连接数满足要求。
否则,如果发生错误onFatalError,说明有可能创建连接发生错误,则调用emptySignal(),检查并继续创建连接。
Druid连接回收部分的代码分析完毕!
小结
通过两篇文章学习分析了Druid连接池的初始化及连接回收过程,还有连接获取及关闭两部分重要内容,下一篇文章继续分析。
Thanks a lot!
上一篇 连接池 Druid (一) - 初始化过程
下一篇 连接池 Druid (三) - 获取连接 getConnection