Java连接泄漏检测与监控
数据库连接泄漏会导致连接池耗尽,系统无法响应,必须及时发现和预防。
连接泄漏原因
常见泄漏场景
Java
// 场景1:获取连接未关闭
Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
// 处理结果...
// 忘记关闭conn!连接一直占用
// 场景2:异常时未关闭
Connection conn = dataSource.getConnection();
try {
Statement stmt = conn.createStatement();
stmt.executeUpdate("INSERT INTO users VALUES(...)");
// 异常发生,conn未关闭
} catch (SQLException e) {
throw e; // conn泄漏!
}
// 场景3:在循环中获取未关闭
for (User user : users) {
Connection conn = dataSource.getConnection();
// 处理...
// 循环100次,泄漏100个连接
}
// 场景4:finally未正确关闭
Connection conn = dataSource.getConnection();
try {
// ...
} finally {
conn.close(); // close()可能抛异常!
// 如果这里异常,后续清理不执行
}
正确的连接管理
try-with-resources
Java
// Java 7+ 推荐:自动关闭
try (Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
// 处理结果
}
} // 自动关闭conn、pstmt、rs
try-finally嵌套
Java
// Java 6 或需要单独处理异常
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = dataSource.getConnection();
pstmt = conn.prepareStatement(sql);
rs = pstmt.executeQuery();
while (rs.next()) {
// 处理
}
} catch (SQLException e) {
throw e;
} finally {
// 按顺序关闭,各自try-catch
if (rs != null) try { rs.close(); } catch (SQLException e) {}
if (pstmt != null) try { pstmt.close(); } catch (SQLException e) {}
if (conn != null) try { conn.close(); } catch (SQLException e) {}
}
HikariCP泄漏检测
配置泄漏检测
Java
HikariConfig config = new HikariConfig();
// 泄漏检测阈值(毫秒)
config.setLeakDetectionThreshold(60000); // 60秒
// 如果连接借用超过60秒未归还,记录警告日志
HikariDataSource dataSource = new HikariDataSource(config);
泄漏检测原理
Java
┌─────────────────────────────────────┐
│ HikariCP泄漏检测 │
├─────────────────────────────────────┤
│ │
│ 1. 获取连接时记录时间戳 │
│ │
│ 2. 连接归还时检查持有时间 │
│ │
│ 3. 持有时间 > threshold │
│ → 记录警告日志 │
│ → 显示获取位置的堆栈 │
│ │
└─────────────────────────────────────┘
泄漏日志示例
Java
警告日志:
Connection leak detection triggered for connection...
Last connection acquisition trace:
at com.example.UserService.getUser(UserService.java:25)
at com.example.Controller.handle(Controller.java:10)
...
指出连接泄漏的代码位置
检测阈值设置
Java
// 设置建议
// 0:禁用泄漏检测
// >0:启用检测,超过阈值报警
// 开发环境:较小阈值,及时发现
config.setLeakDetectionThreshold(10000); // 10秒
// 生产环境:适当阈值,避免误报
config.setLeakDetectionThreshold(120000); // 2分钟
Druid泄漏检测
配置监控
Java
DruidDataSource dataSource = new DruidDataSource();
// 开启监控统计
dataSource.setFilters("stat");
// removeAbandoned:自动回收泄漏连接
dataSource.setRemoveAbandoned(true); // 开启
dataSource.setRemoveAbandonedTimeout(180); // 180秒回收
dataSource.setLogAbandoned(true); // 记录日志
// 泄漏连接自动回收并记录堆栈
Druid监控页面
Java
// 配置StatViewServlet
@Bean
public ServletRegistrationBean druidStatViewServlet() {
ServletRegistrationBean bean = new ServletRegistrationBean(
new StatViewServlet(), "/druid/*");
return bean;
}
// 访问 /druid/datasource.html 查看:
// - ActiveCount:活跃连接数
// - PoolingCount:池中连接数
// - ErrorCount:错误次数
// - ExecuteCount:执行次数
连接池监控指标
HikariCP监控
Java
HikariPoolMXBean pool = dataSource.getHikariPoolMXBean();
// 关键指标
int active = pool.getActiveConnections(); // 活跃连接
int idle = pool.getIdleConnections(); // 空闲连接
int waiting = pool.getThreadsAwaitingConnection(); // 等待线程
int total = pool.getTotalConnections(); // 总连接
// 监控判断
if (waiting > 0) {
// 有线程等待连接,池可能太小
}
if (active >= maxPoolSize) {
// 活跃连接达到上限,可能有泄漏或池太小
}
JMX监控
Java
// JMX连接
MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
ObjectName name = new ObjectName("com.zaxxer.hikari:type=Pool (MyPool)");
HikariPoolMXBean pool = JMX.newMXBeanProxy(mBeanServer, name, HikariPoolMXBean.class);
// 获取指标
int active = pool.getActiveConnections();
Micrometer/Prometheus监控
Java
// Spring Boot集成
@Bean
HikariDataSource dataSource(HikariConfig config) {
return new HikariDataSource(config);
}
// 自动暴露指标到Prometheus
// hikaricp_connections_active
// hikaricp_connections_idle
// hikaricp_connections_pending
// hikaricp_connections_max
// Grafana可视化
自定义泄漏检测
连接包装器
Java
public class LeakDetectionConnection implements Connection {
private Connection delegate;
private long borrowTime;
private String borrowStack;
public LeakDetectionConnection(Connection delegate) {
this.delegate = delegate;
this.borrowTime = System.currentTimeMillis();
this.borrowStack = Thread.currentThread().getStackTrace().toString();
}
@Override
public void close() throws SQLException {
long holdTime = System.currentTimeMillis() - borrowTime;
if (holdTime > 60000) { // 持有超过60秒
log.warn("连接持有时间过长: {}ms, 堆栈: {}", holdTime, borrowStack);
}
delegate.close();
}
// 其他方法委托delegate...
}
连接池包装
Java
public class LeakDetectionDataSource implements DataSource {
private DataSource delegate;
@Override
public Connection getConnection() throws SQLException {
Connection conn = delegate.getConnection();
return new LeakDetectionConnection(conn); // 包装检测
}
}
定期巡检
检查活跃连接
YAML
// 定时检查活跃连接数
@Scheduled(fixedRate = 60000)
public void checkConnections() {
HikariPoolMXBean pool = dataSource.getHikariPoolMXBean();
int active = pool.getActiveConnections();
int waiting = pool.getThreadsAwaitingConnection();
if (active > threshold || waiting > 0) {
log.warn("连接池异常: 活跃={}, 等待={}", active, waiting);
// 发送告警
}
}
检查连接池配置
text
// 检查配置是否合理
public void validatePoolConfig() {
if (dataSource.getMaximumPoolSize() < expectedMax) {
log.warn("连接池最大值太小");
}
if (dataSource.getConnectionTimeout() > 30000) {
log.warn("连接超时设置过长");
}
if (dataSource.getLeakDetectionThreshold() == 0) {
log.warn("泄漏检测未开启");
}
}
监控告警
Prometheus告警规则
text
# alertmanager规则
groups:
- name: datasource
rules:
- alert: HikariCPConnectionsExhausted
expr: hikaricp_connections_active >= hikaricp_connections_max
for: 1m
annotations:
summary: "连接池耗尽"
- alert: HikariCPConnectionsPending
expr: hikaricp_connections_pending > 0
for: 30s
annotations:
summary: "有线程等待连接"
注意事项
所有获取的连接必须关闭(归还)
try-with-resources是最佳实践
HikariCP泄漏检测阈值不要太大(避免漏报)也不要太小(避免误报)
生产环境必须开启连接池监控
定期巡检连接池状态,及时发现异常
泄漏检测只是辅助,核心是规范编码
要点总结
- 连接泄漏:获取连接未归还,池耗尽
- try-with-resources自动关闭,最佳实践
- HikariCP leakDetectionThreshold检测泄漏
- Druid removeAbandoned自动回收泄漏连接
- 监控指标:active、idle、waiting,异常告警
📝 发现内容有误?点击此处直接编辑