com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 60000, active 20, maxActive 20, creating 0

活动的连接数为20, 最大的连接数为20, 活动的连接数与最大连接数相同,连接池用完了,在等待60秒后,没有新连接可用,然后超时了。

stat监控页面显示,活跃连接数很高不释放。CPU超过100%。

当程序存在缺陷时,申请的连接忘记关闭,这时候,就存在连接泄漏了。

比如Connection connection = jdbcTemplate.getDataSource().getConnection(); 这样得到的连接spring不会再帮你关闭,你需要手动关闭。

1、properties配置:

#druid recycle Druid的连接回收机制
#超过时间限制是否回收
spring.datasource.druid.removeAbandoned = true
#超时时间;单位为秒。180秒=3分钟
spring.datasource.druid.removeAbandonedTimeout = 180
#关闭abanded连接时输出错误日志
spring.datasource.druid.logAbandoned = true

2、xml配置:

<!-- 超过时间限制是否回收 -->  
<property name="removeAbandoned" value="true" />  
<!-- 超时时间;单位为秒。180秒=3分钟 -->  
<property name="removeAbandonedTimeout" value="180" />  
<!-- 关闭abanded连接时输出错误日志 -->  
<property name="logAbandoned" value="true" />

此配置项会影响性能,只在排查的时候打开,系统运行时最好关闭。 

此项配日志会将连接泄漏位置打印出来,手动关闭泄露位置的连接就行了。

以下为错误示例就会造成连接泄漏:

public class Test5 {
    @Autowired
    private JdbcTemplate jdbcTemplate;
 
    private void test() {
        Connection connection = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            for (int i = 0; i < 2; i++) {
                connection = jdbcTemplate.getDataSource().getConnection();
                // 第一次使用connection
                ps = connection.prepareStatement("select 1 from dual");
                rs = ps.executeQuery();
                while (rs.next()) {
                    System.out.println(rs.getString(1));
                }
                // 连接connection可以一直使用,避免频繁创建connection消耗资源
                // ps、rs记得要关闭
                rs.close();
                ps.close();
 
                // 第二次使用connection
                ps = connection.prepareStatement("select 1 from dual");
                rs = ps.executeQuery();
                while (rs.next()) {
                    System.out.println(rs.getString(1));
                }
                // 连接connection可以一直使用,避免频繁创建connection消耗资源
                // ps、rs记得要关闭
                rs.close();
                ps.close();
 
                // 第三次使用connection
                ps = connection.prepareStatement("select 1 from dual");
                rs = ps.executeQuery();
                while (rs.next()) {
                    System.out.println(rs.getString(1));
                }
                // 连接connection可以一直使用,避免频繁创建connection消耗资源
                // ps、rs记得要关闭
                rs.close();
                ps.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
 
    }
}

粗略一看没什么问题,finally最终一定会关闭连接,但是仔细看连接在for循环中打开的,实际打开了2个连接只关闭了一个,最终每调用一次该方法就会泄漏一次连接。将connection = jdbcTemplate.getDataSource().getConnection();放在循环外面就解决了。

错误示例二:

public class Test5 {
    @Autowired
    private JdbcTemplate jdbcTemplate;
 
    private void test() {
        Connection connection = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            connection = jdbcTemplate.getDataSource().getConnection();
            ps = connection.prepareStatement(sql);
            rs = ps.executeQuery();
            while (rs.next()) {
                if (rs.getInt(1) == 1) {                    
                    test();// 递归
                }
            }
            rs.close();
            ps.close();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
 
    }
}

递归也可能会导致,连接数不够。当递归函数需要递归很多次,只有递归结束才开始关闭连接,这期间活跃连接数陡增,若果这个函数被并发调用更恐怖。正确的做法,将connection作为递归函数的参数传递进去。

// isClosed()即使断开连接也会返回false,只有close()后才返回true。
 // isValid(1)单位秒,连接失效返回false。

if (connection == null || connection.isClosed() || !connection.isValid(1)) {
        connection = jdbcTemplate.getDataSource().getConnection();
 }

一般一个connection可以创建300个preparedstatement同时使用,一个connection最大并行299个preparedstatement,记住preparedstatement用完后要关闭,connection每创建一个preparedstatement就相当于打开一个游标,超过300个就会报connection打开游标超出最大数(ORA-01000: 超出打开游标的最大数)。每个线程的preparedstatement应该是并行运行的。

所以体现出来spring管理connection连接的好处,不用去关心连接是否已经手动关闭。当项目中大量使用手动维护connection连接时,成千上万代码就会难免忘记关闭connection造成泄漏。

druid 配置参考 : DruidDataSource配置 · alibaba/druid Wiki · GitHub阿里云计算平台DataWorks(https://help.aliyun.com/document_detail/137663.html) 团队出品,为监控而生的数据库连接池 - DruidDataSource配置 · alibaba/druid Wikihttps://github.com/alibaba/druid/wiki/druiddatasource%E9%85%8D%E7%BD%AE