一、为什么连接池会泄漏

咱们先来聊聊这个烦人的连接泄漏问题。想象一下,你开了一家奶茶店,店里准备了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默认不会自动回收这些连接,最终连接池就会耗尽。

二、诊断连接泄漏的实用技巧

发现连接泄漏时,别急着重启服务器。咱们有几个侦探工具可以用:

  1. Tomcat自带监控:在/manager/status页面可以看到活跃连接数
  2. JMX工具:通过JConsole查看jdbc相关MBean
  3. 日志分析:配置连接池的removeAbandonedTimeout参数

这里有个诊断示例(技术栈同上):

// 在context.xml中配置监控参数
<Resource name="jdbc/mydb" 
          removeAbandoned="true"
          removeAbandonedTimeout="60"
          logAbandoned="true"
          ... />

当连接超过60秒未关闭,Tomcat会:

  1. 自动回收该连接
  2. 在日志中记录堆栈跟踪
  3. 帮你定位是哪段代码忘记关闭连接

三、完整解决方案:防御性编程

解决泄漏问题要像写遗嘱一样严谨——确保任何情况下资源都能释放。推荐三种写法:

方案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"        <!-- 记录泄漏日志 -->
          />

特别注意

  1. testOnBorrow会影响性能,生产环境建议用testWhileIdle
  2. MySQL默认8小时断开空闲连接,需要设置validationInterval
  3. 连接数不是越大越好,通常20-50足够应付中小型应用

五、实战中的经验教训

去年我们电商系统在大促时发生过严重泄漏,总结几点教训:

  1. 不要信任框架:即使使用Hibernate,错误配置也会导致泄漏
  2. 监控是必须的:建议配置以下监控项:
    • 活跃连接数
    • 等待获取连接的线程数
    • 连接等待时间
  3. 压力测试:用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());
}

六、总结与最佳实践

经过多次实战,我们团队现在遵循这些规范:

  1. 代码规范

    • 强制使用try-with-resources
    • 禁止直接操作Connection
    • DAO层统一使用Spring模板
  2. 配置规范

    • 生产环境必须配置泄漏检测
    • 设置合理的连接超时(30-120秒)
    • 定期检查validationQuery是否有效
  3. 运维规范

    • 每天检查连接池监控
    • 每周分析泄漏日志
    • 重大活动前进行压力测试

记住,连接池就像游泳池的救生圈——既要保证够用,又要及时回收。养成良好的编码习惯,才能让系统游刃有余。