一、为什么连接池会泄漏
咱们先来聊聊这个烦人的连接泄漏问题。想象一下,你开了一家奶茶店,店里准备了10个固定员工(数据库连接)。正常情况下,10个员工足够应付客流。但如果有顾客点完单不取奶茶(不释放连接),很快你的员工就会不够用,新顾客就得干等着。
在Java Web应用中,Tomcat连接池泄漏就是这么回事。来看个典型错误示例(技术栈:Java + Tomcat + MySQL):
// 错误示例:没有正确关闭连接
public List<User> getUsers() throws SQLException {
Connection conn = dataSource.getConnection(); // 获取连接
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
List<User> users = new ArrayList<>();
while (rs.next()) {
// 假设这里发生了异常...
users.add(new User(rs.getString("name")));
}
// 忘记关闭rs/stmt/conn
return users;
}
你看,如果while循环里抛出异常,这些资源就永远无法回收了。更糟的是,Tomcat默认不会自动回收这些连接,最终连接池就会耗尽。
二、诊断连接泄漏的实用技巧
发现连接泄漏时,别急着重启服务器。咱们有几个侦探工具可以用:
- Tomcat自带监控:在
/manager/status页面可以看到活跃连接数 - JMX工具:通过JConsole查看
jdbc相关MBean - 日志分析:配置连接池的
removeAbandonedTimeout参数
这里有个诊断示例(技术栈同上):
// 在context.xml中配置监控参数
<Resource name="jdbc/mydb"
removeAbandoned="true"
removeAbandonedTimeout="60"
logAbandoned="true"
... />
当连接超过60秒未关闭,Tomcat会:
- 自动回收该连接
- 在日志中记录堆栈跟踪
- 帮你定位是哪段代码忘记关闭连接
三、完整解决方案:防御性编程
解决泄漏问题要像写遗嘱一样严谨——确保任何情况下资源都能释放。推荐三种写法:
方案1:传统try-catch-finally
public List<User> getUsers() throws SQLException {
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
conn = dataSource.getConnection();
stmt = conn.createStatement();
rs = stmt.executeQuery("SELECT * FROM users");
List<User> users = new ArrayList<>();
while (rs.next()) {
users.add(new User(rs.getString("name")));
}
return users;
} finally {
// 倒序关闭:后开的先关
if (rs != null) try { rs.close(); } catch (SQLException ignore) {}
if (stmt != null) try { stmt.close(); } catch (SQLException ignore) {}
if (conn != null) try { conn.close(); } catch (SQLException ignore) {}
}
}
方案2:Java 7+的try-with-resources
public List<User> getUsersModern() throws SQLException {
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users")) {
List<User> users = new ArrayList<>();
while (rs.next()) {
users.add(new User(rs.getString("name")));
}
return users;
} // 自动关闭所有资源
}
方案3:使用Spring的JdbcTemplate
@Repository
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
public List<User> getUsers() {
return jdbcTemplate.query("SELECT * FROM users", (rs, rowNum) ->
new User(rs.getString("name")));
// 无需手动处理连接
}
}
四、高级配置与注意事项
即使代码写对了,配置不当也会出问题。以下是Tomcat连接池的关键参数:
<Resource name="jdbc/mydb"
type="javax.sql.DataSource"
factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/mydb"
username="user"
password="pass"
initialSize="5" <!-- 初始连接数 -->
maxActive="20" <!-- 最大活跃连接数 -->
maxIdle="10" <!-- 最大空闲连接数 -->
minIdle="5" <!-- 最小空闲连接数 -->
maxWait="10000" <!-- 获取连接超时时间(ms) -->
testOnBorrow="true" <!-- 借用时验证连接 -->
validationQuery="SELECT 1" <!-- 简单的验证SQL -->
removeAbandoned="true" <!-- 开启泄漏回收 -->
removeAbandonedTimeout="60"<!-- 60秒视为泄漏 -->
logAbandoned="true" <!-- 记录泄漏日志 -->
/>
特别注意:
testOnBorrow会影响性能,生产环境建议用testWhileIdle- MySQL默认8小时断开空闲连接,需要设置
validationInterval - 连接数不是越大越好,通常20-50足够应付中小型应用
五、实战中的经验教训
去年我们电商系统在大促时发生过严重泄漏,总结几点教训:
- 不要信任框架:即使使用Hibernate,错误配置也会导致泄漏
- 监控是必须的:建议配置以下监控项:
- 活跃连接数
- 等待获取连接的线程数
- 连接等待时间
- 压力测试:用JMeter模拟长时间运行,观察连接数变化
这里有个监控代码示例:
// 获取连接池状态
public void monitorPool() {
org.apache.tomcat.jdbc.pool.DataSource ds =
(org.apache.tomcat.jdbc.pool.DataSource) dataSource;
System.out.println("活跃连接: " + ds.getNumActive());
System.out.println("空闲连接: " + ds.getNumIdle());
System.out.println("等待线程: " + ds.getWaitCount());
}
六、总结与最佳实践
经过多次实战,我们团队现在遵循这些规范:
代码规范:
- 强制使用try-with-resources
- 禁止直接操作Connection
- DAO层统一使用Spring模板
配置规范:
- 生产环境必须配置泄漏检测
- 设置合理的连接超时(30-120秒)
- 定期检查validationQuery是否有效
运维规范:
- 每天检查连接池监控
- 每周分析泄漏日志
- 重大活动前进行压力测试
记住,连接池就像游泳池的救生圈——既要保证够用,又要及时回收。养成良好的编码习惯,才能让系统游刃有余。
评论