起因
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,解决问题的过程中遇到了很多的问题,不断的翻源码找到合适的方法,问题得到了解决.