起因
pg數據庫的連接無法回收,並且某一連接如果查詢的次數過度會占用很多的內存,最終導致內存溢出
解決思路
利用Druid的過濾器的機制,先找到統計連接的使用次數的參數,設定到一定次數之后手動斷開連接.
開始解決
選擇了statementExecuteQueryAfter()這個鈎子函數作為切入點,這個函數是在執行完事務之后調用的,獲取到了連接執行connection.close(),查看druid的監控和數據庫發現數據連接的pid變更了,證明connection.close()的關閉方法是有效的.但是關閉后會報一個數據庫連接已關閉的異常,暫時放到后面解決.
第二步要找到連接的使用次數,因為並不是每個連接查詢完都需要進行關閉,而是查詢到一定次數之后,最后在connectionHolder.getUseCount()方法中獲取到了useCount.
第一次測試,發現連接不見了
單線程的測試通過了,進行並發測試發現.close()的方法關閉后最后池里只會有一個連接不斷地銷毀創建.所有的線程會爭搶這一個連接
更換鈎子函數
查看代碼的過程看到了更符合業務需求的鈎子函數dataSource_releaseConnection,從這個方法中去斷開連接似乎是個更好的選擇
嘗試使用Abandoned解決問題
鑒於close()方法會拋出異常並且不會創建連接,換了一個思路嘗試使用druid自己的機制嘗試進行連接的回收.
進行測試發現該方法並沒有像想象中的進行回收
再次嘗試Connection.close()
abandond()方法解決無果,再次嘗試Connection.close(),現在有兩個問題亟需解決,一個是連接關閉后需要再生成一個新的連接添加回池里面解決Connection.close()到一定程度后池里只有一個連接的問題,第二個問題就是解決拋異常的問題.
解決沒有連接的問題: 擼源碼找到了DruidDataSource的createPhysicalConnection()方法,和protected修飾的put方法,既然是protected,第一反應自定義一個MyDruidDataSource實現put方法
自定義MyDruidDataSource
@Override
public boolean put(PhysicalConnectionInfo physicalConnectionInfo) {
return super.put(physicalConnectionInfo);
}
進行測試,發現可以正常的put連接,解決了.close()刪除連接的問題.
開始解決第二個拋出異常的問題: 異常是在下面這段代碼拋出去的
super.dataSource_releaseConnection(chain, connection);
追蹤源碼發現,會檢查線程是否會關閉,如果關閉了則拋出連接已經關閉的錯誤.
解決方法: 既然已經關閉了連接那就不需要往下執行recycle方法回收連接了,所以當我們關閉連接的時候直接return,不再執行recycle方法
更換Connetion.close(),Druid有更優雅的關閉連接的方法:druidDataSource.discardConnection(connection.getConnectionHolder()); 該方法可以更新活躍數量
釋放代碼:
public void dataSource_releaseConnection(FilterChain chain, DruidPooledConnection connection) throws SQLException {
if (connection != null && connection.getConnectionHolder() != null && connection.getConnectionHolder().getUseCount() >= 1) {
System.out.println("-----------存活超過 maxUseCount 使用次數限制,強制釋放連接------------");
//存活超過 maxUseCount 使用次數限制,強制釋放連接
DruidAbstractDataSource.PhysicalConnectionInfo physicalConnection = connection.getConnectionHolder().getDataSource().createPhysicalConnection();
MyDruidDataSource druidDataSource = (MyDruidDataSource) connection.getConnectionHolder().getDataSource();
druidDataSource.put(physicalConnection);
druidDataSource.discardConnection(connection.getConnectionHolder());
return;
}
super.dataSource_releaseConnection(chain, connection);
}
注意要把MyDruidDataSource注入到容器.
springboot測試成功
進行正常測試通過,進行疲勞測試通過, 連接被關閉,內存穩定沒有oom的問題.成功.
和公司框架進行集成
和公司框架進行集成,發現公司框架自定義了druid的配置類,如果使用現在的自定義的MyDruidDataSource的方法和公司框架有沖突.需要修改公司框架的源碼,並且后期維護是個問題.
考慮使用反射的方式解決與公司框架集成的問題.
使用反射,直接反射put方法,不需要使用自定義的MyDruidDataSource實現了.無縫與公司框架集成
Class<?> directDataSourceClass = druidDataSource.getClass().getSuperclass();
Method put = directDataSourceClass.getDeclaredMethod("put", DruidAbstractDataSource.PhysicalConnectionInfo.class);
put.setAccessible(true);
put.invoke(druidDataSource, physicalConnection);
總結
目前測試表現穩定,數據庫連接按照期望的方式回收了,沒有再出現過oom,解決問題的過程中遇到了很多的問題,不斷的翻源碼找到合適的方法,問題得到了解決.
