连接池耗尽事故:finally块里的"安全代码"是怎么泄漏的
先说个真事:某天夜里服务报警MySQL连接数打满。查了半天,发现问题代码每次只漏一个连接,白天流量一大池子当场去世。根因藏在finally块里——一段看起来"绝对安全"的关闭代码。
泄漏是怎么发生的
看这段典型代码:
Connection conn = null;
PreparedStatement ps = null;
try {
conn = dataSource.getConnection();
ps = conn.prepareStatement(sql);
rs = ps.executeQuery();
} finally {
if (rs != null) rs.close();
if (ps != null) ps.close(); // 问题在这!
if (conn != null) conn.close();
}
finally不是必定执行吗?但你忽略了关键:ps.close()抛异常,conn.close()就不执行了。网络抖动时PS关闭失败,异常被catch吞掉,表面风平浪静,底下连接一个一个漏。
正确写法:try-with-resources
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
// ... 业务逻辑 ...
} catch (Exception e) {
log.error("失败", e);
}
// 无需finally!close()失败自动追加到suppressed,不阻断其他资源
Java 7引入的try-with-resources,每个资源close()失败自动追加到suppressed列表,不阻断其他资源关闭。用了它基本告别连接泄漏。
必须手动管连接时的正确姿势
Connection conn = null;
try {
conn = dataSource.getConnection();
} finally {
if (conn != null)
try { conn.close(); }
catch (SQLException e) { log.error("关闭失败", e); }
}
只关conn,别关ps。JDBC规范:关闭Connection会级联关闭由它创建的所有Statement。先关ps失败会阻断conn关闭,只关conn是最安全的选择。
防泄漏三件套
1. 测试环境开泄漏检测:leak-detection-threshold设为30000,HikariCP会在30秒未归还时打日志。
2. 合理配置,别贪大:小服务10-20个连接够用,connection-timeout设30秒防无限等。
3. catch异常必须log:finally块里catch了异常却不log,是最危险的自欺欺人。
血的教训
finally块里catch异常然后静默,是最危险的自欺欺人。它让错误静默发生,问题难以定位。catch可以,log一定要打。
测试环境几乎不会复现这类问题。连接每次正常还,看不出任何毛病。但生产环境流量一冲,半夜没事,白天去世。
连接池不是越大越好。每个连接占MySQL资源,池太大会让MySQL疲于应付连接管理,性能反而下降。小服务10-20个连接够用了。
没做过生产故障复盘的代码,等于没写过。下期见。