一、問題背景
二、源碼分析
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失效的因素,作為配置的一個考量。
