记一次解决postgresql数据库内存泄露的问题

起因

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

posted @ 2021-05-19 15:01  lbr617  阅读(1293)  评论(0编辑  收藏  举报