一、問題背景
二、源碼分析

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失效的因素,作為配置的一個考量。