一、问题背景
二、源码分析

DestroyConnectionThread做的事情很简单,就是每隔固定的时间去执行一下DestoryTask的run方法,执行的间隔时间基于druid配置timeBetweenEvictionRunsMillis的值:
DestoryTask的run方法调用shrink方法,该方法是空闲连接检查的核心方法,至于removeAbandoned方法是用于回收借出去但一直未归还的连接(这种连接可能导致连接泄露),它与druid的配置removeAbandoned有关,这里就不细讲了:

shrink方法逻辑如下:
public void shrink(boolean checkTime, boolean keepAlive) { //加锁 try { lock.lockInterruptibly(); } catch (InterruptedException e) { return; } int evictCount = 0; //需要剔除的个数 int keepAliveCount = 0; //需要保持会话的个数 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]; //DestroyThread调用shrink时,checkTime=true,keepAlive基于配置的值(默认为false) if (checkTime) { //phyTimeoutMillis参数(默认值为-1)设定了一条物理连接的存活时间, //不同的数据库对一个连接有最大的维持时间,比如mysql是8小时,设置该 //参数是为了防止应用获取某连接时,该连接在数据库侧已关闭而导致异常。 if (phyTimeoutMillis > 0) { //如果某条连接已超过phyTimeoutMillis,则将其放入需要剔除的连接数组evictConnections中 long phyConnectTimeMillis = currentTimeMillis - connection.connectTimeMillis; if (phyConnectTimeMillis > phyTimeoutMillis) { evictConnections[evictCount++] = connection; continue; } } //获取连接空闲时间 long idleMillis = currentTimeMillis - connection.lastActiveTimeMillis; //如果某条连接空闲时间小于minEvictableIdleTimeMillis,则不用继续检查剩下的连接了 if (idleMillis < minEvictableIdleTimeMillis) { break; } //判断此连接的状态,将其放入不同处理的连接数组中 if (checkTime && i < checkCount) { //这里checkTime有点多余,一定为true,因为它是if(checkTime)分支中的逻辑 //如果此连接仍在checkCount范围之内,即它是一个多出最小空闲连接数的连接, //那么就将它加入到需要剔除的连接数组evictConnections中 evictConnections[evictCount++] = connection; } else if (idleMillis > maxEvictableIdleTimeMillis) { //如果连接空闲时间已经大于maxEvictableIdleTimeMillis,也将它加入到需要 //剔除的连接数组evictConnections中 evictConnections[evictCount++] = connection; } else if (keepAlive) { //如果连接超过checkCount范围,并且空闲时间小于maxEvictableIdleTimeMillis, //并且开启了keepAlive,那么就将它加入到需要维持的连接数组keepAliveConnections中 keepAliveConnections[keepAliveCount++] = connection; } } else { //对于不需要checkTime的情形,就非常简单了,将比minIdle连接数多的连接放入 //需要剔除的连接数组evictConnections中 if (i < checkCount) { evictConnections[evictCount++] = connection; } else { break; } } } //剔除连接和需要维持的连接都作为被移出连接,然后对连接池中的connections元素进行移动, //使得有用的连接重新放在连接数组connections的头部,并将其余元素置为null int removeCount = evictCount + keepAliveCount; if (removeCount > 0) { System.arraycopy(connections, removeCount, connections, 0, poolingCount - removeCount); Arrays.fill(connections, poolingCount - removeCount, poolingCount, null); poolingCount -= removeCount; } keepAliveCheckCount += keepAliveCount; } finally { lock.unlock(); } //处理需要剔除的连接数组evictConnections,对其中的连接进行关闭, //并维护监控指标:destroyCountUpdater,然后将evictConnections清空 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); } //处理需要维持连接的连接数组keepAliveConnections if (keepAliveCount > 0) { //维护监控指标 this.getDataSourceStat().addKeepAliveCheckCount(keepAliveCount); for (int i = keepAliveCount - 1; i >= 0; --i) { DruidConnectionHolder holer = keepAliveConnections[i]; Connection connection = holer.getConnection(); //更新连接的keepAlive检查计数器 holer.incrementKeepAliveCheckCount(); boolean validate = false; try { //使用配置的validationQuery Sql检查当前连接是否有效,validateConnection //方法非常简单,如果检查过程中抛出异常都会被此处catch住并处理 this.validateConnection(connection); validate = true; } catch (Throwable error) { if (LOG.isDebugEnabled()) { LOG.debug("keepAliveErr", error); } // skip } if (validate) { //如果连接有效性检查成功,则更新连接的最近活跃时间,并尝试将连接放回连接池, //put(holder)不一定保证放回成功,在连接池已满的情况下将不会放入,方法中通过 //使用条件变量以及poolingPeak等机制保证了连接不会被泄露 holer.lastActiveTimeMillis = System.currentTimeMillis(); put(holer); } else { //如果连接有效性检查失败,则关闭此连接 JdbcUtils.close(connection); } } //清空连接数组keepAliveConnections Arrays.fill(keepAliveConnections, null); } }
三、验证结论
根据调试过程中的源码分析,可知druid_1.1.10判断连接是否销毁还是保活的逻辑如下(只讨论checkTime为true的情况):

到这里,我们就可以下一个结论了:druid对于空闲连接还是有可能回收的,只要它未开启keepAlive并且闲置时间过长就会回收空闲连接,从而使得连接池中的连接数小于配置的minIdle值。
为了验证结论,我开启了druid monitor的web页面访问,然后在如下的页面中去观察池中连接的情况:

与druid空闲连接回收的相关参数配置如下图:

首先不开启keepAlive功能(druid也是默认关闭的),在应用启动的时候,从druid monitor中观察到连接池中的连接数如下:

等待大约2~3分钟之后(再此期间不要发起任何数据库请求),再次观察连接池中的连接数,可以发现连接数为0:

接着配置"spring.datasource.druid.keep-alive=true"以打开keepAlive,重启应用并重复上述过程,结果如下:

可以发现keepAlive起作用了,池中连接数维持在20,结论得到验证。接着回过头去查看了一下maxEvictableIdleTimeMillis这个参数的默认值为25200000,刚好7个小时,差不多能和DBA监测到的连接降低时间对上。
四、其他发现

然而实际上代码体现出来的逻辑并不是这么一回事,maxEvictableIdleTimeMillis更像起到了决定性的作用。
//1.满足下面不等式 maxEvictableIdleTimeMillis - minEvictableIdleTimeMillis <= timeBetweenEvictionRunsMillis //2.连接一直处于未使用状态,那么在空闲时间小于minEvictableIdleTimeMillis之前,连接的lastActiveTimeMillis都不会被更新
下面是我的一个测试,druid相关配置情况如图:

启用应用并静静等待1~2分钟,通过druid monitor查看连接池状态:

通过浏览器调用一个http查询接口,连接池连接数恢复:

静静等待1~2分钟,可以看到连接池中的连接又被清空:

结论:虽然maxEvictableIdleTimeMillis这个参数我们一般不配置,它的默认值也比较大(7小时),但是实际在配置druid时,还是建议考虑keepAlive失效的因素,作为配置的一个考量。