作者 lucy · 2026-04-11
⚠️ 声明:本文相关内容仅供参考,实际效果因场景不同可能差异很大,请结合自身情况判断,谨慎参考。
故障背景
某日下午,线上监控系统突然告警:大量用户反馈页面加载缓慢,部分接口直接超时。登录运维后台一看,数据库连接数已经打满,所有新请求全部在等待连接,最终导致服务雪崩。
这是一套基于 Java 的后端服务,使用 Druid 连接池,数据库为 MySQL 8.0。单台应用服务器 QPS 不高,但故障发生时连接池里的连接全部处于 Active 状态,没有任何空闲连接可用。
初步排查
先用 show processlist 查看当前所有连接状态:
mysql> show processlist;
+-----+--------------+--------------------+--------+---------+------+----------+-----------------------+
| Id | User | Host | db | Command | Time | State | Info |
+-----+--------------+--------------------+--------+---------+------+----------+-----------------------+
| 301 | app_user | 10.0.x.x:12345 | myapp | Sleep | 892 | | SELECT * FROM orders |
| 302 | app_user | 10.0.x.x:12345 | myapp | Sleep | 891 | | SELECT * FROM orders |
| 303 | app_user | 10.0.x.x:12345 | myapp | Sleep | 890 | | SELECT * FROM orders |
| ... (大量 Sleep 状态的连接,Time 持续增长)|
+-----+--------------+--------------------+--------+---------+------+----------+-----------------------+
大量连接处于 Sleep 状态,且 Time 字段持续增长,说明这些连接已经在数据库端空等了很长时间没有被释放。连接池配置的最大连接数是 50,此时已经全部耗尽。
根因定位
查看应用日志,发现大量类似错误:
Caused by: java.sql.SQLNonTransientConnectionException: Could not retrieve transaction isolation at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:123) ... Caused by: com.mysql.cj.jdbc.exceptions.CommunicationsException: The last packet successfully received from the server was 45,321 milliseconds ago.结合日志和代码,很快定位到问题所在。项目中有一个订单查询接口,代码大致如下:
@GetMapping("/orders") public List<Order> getOrders(Long userId) { Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = dataSource.getConnection(); ps = conn.prepareStatement("SELECT * FROM orders WHERE user_id = ?"); ps.setLong(1, userId); rs = ps.executeQuery(); List<Order> list = new ArrayList<>(); while (rs.next()) { Order o = new Order(); o.setId(rs.getLong("id")); o.setAmount(rs.getBigDecimal("amount")); list.add(o); } return list; } catch (Exception e) { log.error("查询订单失败", e); return Collections.emptyList(); // ❌ 资源泄漏在这里! } finally { // 正常返回时释放资源 closeQuietly(rs, ps, conn); } }问题出在异常处理分支:当
try块中途抛出异常,且异常被catch捕获后直接return,此时finally块虽然会执行,但closeQuietly的实现如果有任何疏忽,就会导致资源泄漏。再深入看
closeQuietly方法:private void closeQuietly(ResultSet rs, PreparedStatement ps, Connection conn) { try { if (rs != null) rs.close(); } catch (Exception ignored) {} try { if (ps != null) ps.close(); } catch (Exception ignored) {} try { if (conn != null) conn.close(); } catch (Exception ignored) {} }表面看起来没问题,但真正的问题是:当
dataSource.getConnection()抛出SQLException时,conn变量本身就是null,此时finally块里的closeQuietly根本不会被调用,因为finally只在try块进入后才会执行。复盘结论
根因分析:
- 某些边缘场景(如数据库连接创建过程中被中断),导致
conn为null- 外层
catch捕获异常后直接返回,finally未进入- 数据库连接从未被释放,持续占用连接池资源
- 随着请求量增加,连接池资源耗尽,服务雪崩
解决方案
方案一:使用 try-with-resources(推荐)
@GetMapping("/orders") public List<Order> getOrders(Long userId) { // 使用 try-with-resources,自动释放资源 try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement("SELECT * FROM orders WHERE user_id = ?")) { ps.setLong(1, userId); try (ResultSet rs = ps.executeQuery()) { List<Order> list = new ArrayList<>(); while (rs.next()) { Order o = new Order(); o.setId(rs.getLong("id")); o.setAmount(rs.getBigDecimal("amount")); list.add(o); } return list; } } catch (SQLException e) { log.error("查询订单失败", e); return Collections.emptyList(); } }方案二:确保资源安全释放
@GetMapping("/orders") public List<Order> getOrders(Long userId) { Connection conn = null; try { conn = dataSource.getConnection(); // ... 业务逻辑 return list; } catch (Exception e) { log.error("查询订单失败", e); return Collections.emptyList(); } finally { // 无论如何都要释放,即使用户已经返回 if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } } }方案三:使用持久框架
推荐使用 MyBatis / JPA 等持久层框架,由框架统一管理数据库连接的获取和释放,从源头避免手动管理连接的问题。
监控加固
上线修复后,额外增加了以下监控告警:
- 连接池活跃连接数 > 80% 告警
- 单次请求占用连接时长 > 5s 告警
- 定时任务扫描并强制关闭长时间空闲的 Sleep 连接
# 定时清理 MySQL 长时间 Sleep 连接
mysql -uadmin -p -e "
SELECT id, user, host, db, command, time
FROM information_schema.processlist
WHERE command='Sleep' AND time > 300;
" | awk 'NR>1 {print }' | xargs -I{} mysql -uadmin -p -e "KILL {}"
经验总结
- 永远使用 try-with-resources 管理数据库连接、Statement、ResultSet 等资源,不要依赖手动 close
- 连接池要配置合理的监控,提前告警比事后补救成本低得多
- 异常处理不要静默吞掉,至少记录日志并有明确的降级逻辑
- 定期巡检线上连接状态,发现异常及时处理
这类资源泄漏问题在开发环境很难复现,往往在高并发、长时间运行后才暴露。建议在代码审查时重点关注手动资源管理的路径,确保每一条获取的连接都有对应的释放路径。