Java JDBC 连接池资源泄漏问题

39 阅读2分钟

一、Bug 场景

在一个企业级的 Java Web 应用程序中,使用数据库连接池来管理与 MySQL 数据库的连接。应用程序主要负责处理用户的订单操作,包括订单的创建、查询和更新。在高并发的业务场景下,随着时间的推移,系统性能逐渐下降,最终可能导致数据库连接耗尽,新的请求无法获取连接而失败。

二、代码示例

假设使用 HikariCP 连接池,以下是简化的代码片段:

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class JDBCConnectionPoolBug {
    private static HikariDataSource dataSource;

    static {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://localhost:3306/yourdatabase");
        config.setUsername("root");
        config.setPassword("password");
        dataSource = new HikariDataSource(config);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                Connection conn = null;
                PreparedStatement pstmt = null;
                ResultSet rs = null;
                try {
                    conn = dataSource.getConnection();
                    String sql = "SELECT * FROM orders WHERE user_id =?";
                    pstmt = conn.prepareStatement(sql);
                    pstmt.setInt(1, 1);
                    rs = pstmt.executeQuery();
                    while (rs.next()) {
                        System.out.println(rs.getString("order_name"));
                    }
                } catch (SQLException e) {
                    e.printStackTrace();
                } finally {
                    // 错误示范:没有正确关闭资源
                    if (rs != null) {
                        try {
                            rs.close();
                        } catch (SQLException e) {
                            e.printStackTrace();
                        }
                    }
                    if (pstmt != null) {
                        try {
                            pstmt.close();
                        } catch (SQLException e) {
                            e.printStackTrace();
                        }
                    }
                    // 遗漏关闭Connection
                }
            }).start();
        }
    }
}

三、问题描述

  1. 资源未完全关闭:在上述代码的finally块中,虽然关闭了ResultSetPreparedStatement,但遗漏了关闭Connection。这就导致连接对象没有被正确地返回到连接池中,随着每次请求获取新连接而不释放,连接池中的可用连接逐渐减少。
  2. 高并发影响:在高并发环境下,这种资源泄漏问题会被迅速放大。大量线程不断获取连接却不归还,很快连接池中的所有连接都会被占用且无法回收,后续的请求就会因为无法获取连接而抛出SQLException,如Timeout waiting for connection from pool,严重影响系统的正常运行。

四、解决方案

  1. 正确关闭连接:在finally块中确保关闭Connection
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class JDBCConnectionPoolBugFixed {
    private static HikariDataSource dataSource;

    static {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://localhost:3306/yourdatabase");
        config.setUsername("root");
        config.setPassword("password");
        dataSource = new HikariDataSource(config);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                Connection conn = null;
                PreparedStatement pstmt = null;
                ResultSet rs = null;
                try {
                    conn = dataSource.getConnection();
                    String sql = "SELECT * FROM orders WHERE user_id =?";
                    pstmt = conn.prepareStatement(sql);
                    pstmt.setInt(1, 1);
                    rs = pstmt.executeQuery();
                    while (rs.next()) {
                        System.out.println(rs.getString("order_name"));
                    }
                } catch (SQLException e) {
                    e.printStackTrace();
                } finally {
                    if (rs != null) {
                        try {
                            rs.close();
                        } catch (SQLException e) {
                            e.printStackTrace();
                        }
                    }
                    if (pstmt != null) {
                        try {
                            pstmt.close();
                        } catch (SQLException e) {
                            e.printStackTrace();
                        }
                    }
                    if (conn != null) {
                        try {
                            conn.close();
                        } catch (SQLException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
        }
    }
}
  1. 使用try - with - resources语句:从 Java 7 开始,可以使用try - with - resources语句来自动关闭实现了AutoCloseable接口的资源,使代码更简洁且不易出错。
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class JDBCConnectionPoolBugFixedWithTryWithResources {
    private static HikariDataSource dataSource;

    static {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://localhost:3306/yourdatabase");
        config.setUsername("root");
        config.setPassword("password");
        dataSource = new HikariDataSource(config);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                String sql = "SELECT * FROM orders WHERE user_id =?";
                try (Connection conn = dataSource.getConnection();
                     PreparedStatement pstmt = conn.prepareStatement(sql)) {
                    pstmt.setInt(1, 1);
                    try (ResultSet rs = pstmt.executeQuery()) {
                        while (rs.next()) {
                            System.out.println(rs.getString("order_name"));
                        }
                    }
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}