背景介绍
在一次druid数据库连接池连接泄露的排查分析介绍了连接泄露的分析排查过程,在几行代码轻松复现druid连接泄露的BUG之PhyTimeout介绍了当配置了phyTimeoutMillis参数情况下,连接泄露的场景,下面通过代码的方式来复现当数据库操作出现FatalError情况下连接泄露的场景。
复现过程
连接泄露场景
模拟当数据库操作出现FatalError的情况下出现连接泄露的场景。
连接泄露模拟代码
import com.alibaba.druid.pool.DruidConnectionHolder; import com.alibaba.druid.pool.DruidDataSource; import com.alibaba.druid.pool.DruidPooledConnection; import com.alibaba.druid.pool.ExceptionSorter; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.List; import java.util.Map; import java.util.Properties; public class DruidAbandonedCase4OnFatalError { public static void main(String[] args) throws Exception{ DruidDataSource dataSource = new DruidDataSource(); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setUsername("root"); dataSource.setPassword("123456"); dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false"); dataSource.setMinIdle(2); // 保证不会触发DestroyTask,以便手动调用shrink dataSource.setTimeBetweenEvictionRunsMillis(1 * 60 * 60 * 1000); dataSource.setMinEvictableIdleTimeMillis(0); // 用来mock出现FatalError的数据库异常 dataSource.setExceptionSorter(new MockExceptionSorter()); dataSource.init(); // connectioin1为正常的连接 DruidPooledConnection connection1 = dataSource.getConnection(); DruidConnectionHolder holder1 = connection1.getConnectionHolder(); System.out.println(holder1.getConnectionId()); // connectioin2是数据库操作出现异常的连接 DruidPooledConnection connection2 = dataSource.getConnection(); DruidConnectionHolder holder2 = connection2.getConnectionHolder(); System.out.println(holder2.getConnectionId()); // 数据库操作出现FatalError fatalError(connection2); connection2.close(); // connectioin3是正常的连接 DruidPooledConnection connection3 = dataSource.getConnection(); DruidConnectionHolder holder3 = connection3.getConnectionHolder(); System.out.println(holder3.getConnectionId()); connection3.close(); connection1.close(); System.out.println(); System.out.println("调用shrink【之前】,连接池中连接情况,begin"); List<Map<String, Object>> conns = dataSource.getPoolingConnectionInfo(); for(Map<String,Object> conn : conns){ System.out.println(conn.get("connectionId")); } System.out.println("调用shrink【之前】,连接池中连接情况,end"); // 手动执行shrink dataSource.shrink(true); System.out.println(); System.out.println("调用shrink【之后】,连接池中连接情况,begin"); conns = dataSource.getPoolingConnectionInfo(); for(Map<String,Object> conn : conns){ System.out.println(conn.get("connectionId")); } System.out.println("调用shrink【之后】,连接池中连接情况,end"); System.out.println(); System.out.println(holder1.getConnectionId() + " isClosed:" + holder1.getConnection().isClosed()); System.out.println(holder2.getConnectionId() + " isClosed:" + holder2.getConnection().isClosed()); System.out.println(holder3.getConnectionId() + " isClosed:" + holder3.getConnection().isClosed()); } private static void fatalError(Connection conn){ Statement pstmt = null; ResultSet rs = null; try { String sql = "select * from unExistTable"; pstmt = conn.createStatement(); rs = pstmt.executeQuery(sql); while (rs.next()) { int id = rs.getInt("id"); System.out.println("id: " + id); } } catch (SQLException e) { // e.printStackTrace(); } finally { try { if (rs != null) { rs.close(); } if (pstmt != null) { pstmt.close(); } } catch (SQLException e) { // e.printStackTrace(); } } } static class MockExceptionSorter implements ExceptionSorter { @Override public boolean isExceptionFatal(SQLException e) { return true; } @Override public void configFromProperties(Properties properties) { } } }
运行以上程序,输出结果如下:
10001 10002 严重: {conn-10002} discard java.sql.SQLSyntaxErrorException: Table 'test.unexisttable' doesn't exist at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:121) at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) at com.mysql.cj.jdbc.StatementImpl.executeQuery(StatementImpl.java:1200) at com.alibaba.druid.pool.DruidPooledStatement.executeQuery(DruidPooledStatement.java:300) at DruidAbandonedCase4OnFatalError.fatalError(DruidAbandonedCase4OnFatalError.java:82) at DruidAbandonedCase4OnFatalError.main(DruidAbandonedCase4OnFatalError.java:40) 10003 调用shrink【之前】,连接池中连接情况,begin 10003 10001 调用shrink【之前】,连接池中连接情况,end 调用shrink【之后】,连接池中连接情况,begin 10001 调用shrink【之后】,连接池中连接情况,end 10001 isClosed:true 10002 isClosed:true 10003 isClosed:false
代码执行逻辑描述:
- 获取connectionId为1001的连接;
- 获取connectionId为1002的连接,1002在执行数据库操作的时候出现了FatalError,然后调用close方法归还连接;
- 获取connectionId为1003的连接,调用close归还连接;
- 调用close归还connectionId为1001的连接;
- 打印出连接池管理的连接有哪些;
- 调用数据库连接池的shrink方法;
- 打印出连接池管理的连接有哪些;
- 打印出每个连接是否已被关闭。
期望执行结果:
- 由于connectionId为1002的连接发生了FatalError,该连接会被关闭且从连接池中删除;
- 连接池管理的连接包括connectionId为1001和1003,且连接状态都是非关闭状态
实际执行结果:
- connectionId为1002的连接已被关闭且从连接池删除【符合期望】;
- 连接池管理的连接只包括connectionId为1001的连接,不包含connectionId为1003的连接(泄露的连接)【不符合期望】;
- connectionId为1001的连接为已关闭状态(不应该关闭掉)【不符合预期】;
测试了druid 1.1.11,1.2.8-1.2.17,1.2.18-1.2.20,其中1.2.8-1.2.17都存在以上连接泄露的问题,1.1.11,1.2.18-1.2.20不存在以上连接泄露的问题。