跳转至

连接池

本文你会学到

  • 为什么频繁创建物理连接会成为性能瓶颈
  • HikariCP 和 Druid 两大连接池的配置和特点
  • 连接池 vs 直连的使用方式对比
  • 面向 DataSource 标准接口编程的意义

💡 连接池原理

为什么需要连接池?

每次通过 DriverManager.getConnection() 建立物理连接都需要完成 TCP 握手、数据库认证等开销,耗时通常在几毫秒到数十毫秒之间。高并发下频繁创建/销毁连接会成为瓶颈。

sequenceDiagram
    participant App as Java 应用
    participant DB as 数据库

    Note over App,DB: 无连接池(每次请求建立新连接)
    App->>DB: TCP 握手 + 认证(每次都有)
    DB-->>App: 连接建立(耗时几ms~几十ms)
    App->>DB: 执行 SQL
    DB-->>App: 返回结果
    App->>DB: 关闭连接(物理断开)

    Note over App,DB: 有连接池(HikariCP)
    App->>App: 从池中取出已有连接(微秒级)
    App->>DB: 执行 SQL
    DB-->>App: 返回结果
    App->>App: 归还连接到池(不销毁)

🏭 主流实现

Druid 连接池

Druid 是阿里巴巴开源的 JDBC 连接池,除基本连接池功能外,内置了 SQL 监控、慢查询统计、连接池状态可视化等运维能力,在国内 Java 项目中广泛使用。

Druid 连接池基本参数配置
    /**
     * 演示 Druid 连接池基本配置
     * Druid 是阿里巴巴开源的连接池,内置 SQL 监控、慢查询统计等功能,广泛用于国内 Java 项目
     */
    @Test
    void testDruidBasicConfig() throws Exception {
        String url = "jdbc:h2:mem:testdb_druid_basic;DB_CLOSE_DELAY=-1";

        // 创建 DruidDataSource 并配置基本参数
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUrl(url);
        dataSource.setUsername(USER);
        dataSource.setPassword(PASSWORD);
        // 连接池核心参数
        dataSource.setMaxActive(10);          // 最大活跃连接数
        dataSource.setMinIdle(2);             // 最小空闲连接数
        dataSource.setInitialSize(2);         // 初始化连接数
        dataSource.setMaxWait(30000);         // 获取连接最大等待时间(毫秒)
        // 连接有效性检测
        dataSource.setValidationQuery("SELECT 1");
        dataSource.setTestWhileIdle(true);    // 空闲时检测连接有效性

        System.out.println("Druid 连接池配置完成");
        System.out.println("最大活跃连接数: " + dataSource.getMaxActive());
        System.out.println("最小空闲连接数: " + dataSource.getMinIdle());

        // 从 Druid 连接池获取连接并执行查询
        try (Connection conn = dataSource.getConnection();
             Statement stmt = conn.createStatement()) {

            assertNotNull(conn, "从 Druid 连接池获取的连接不应为 null");

            // ResultSet 使用 try-with-resources 确保自动关闭(与最佳实践章节一致)
            try (ResultSet rs = stmt.executeQuery("SELECT 1 AS result")) {
                assertTrue(rs.next());
                assertEquals(1, rs.getInt("result"), "Druid 连接池连接测试查询应返回 1");
                System.out.println("Druid 连接池连接测试成功,查询结果: " + rs.getInt("result"));
            }
        } finally {
            dataSource.close();
        }
    }

Druid 与 HikariCP 核心参数对比:

参数含义 HikariCP Druid
最大连接数 maximumPoolSize maxActive
最小空闲数 minimumIdle minIdle
初始化连接数 initialSize
获取连接超时 connectionTimeout(ms) maxWait(ms)
连接检测 SQL connectionTestQuery validationQuery
HikariCP vs Druid 使用方式对比
    /**
     * 对比 Druid 与 HikariCP 的使用方式:两者均实现 javax.sql.DataSource 接口
     * HikariCP:性能极致,是 Spring Boot 默认连接池
     * Druid:功能丰富,内置监控(SQL 监控、慢查询、连接池状态),适合需要运维可观测性的场景
     */
    @Test
    void testDruidVsHikari() throws Exception {
        String url = "jdbc:h2:mem:testdb_druid_vs;DB_CLOSE_DELAY=-1";

        // === HikariCP ===
        HikariConfig hikariConfig = new HikariConfig();
        hikariConfig.setJdbcUrl(url);
        hikariConfig.setUsername(USER);
        hikariConfig.setPassword(PASSWORD);
        hikariConfig.setMaximumPoolSize(5);

        try (HikariDataSource hikari = new HikariDataSource(hikariConfig);
             Connection conn = hikari.getConnection()) {
            assertNotNull(conn);
            System.out.println("HikariCP 连接类: " + conn.getClass().getName());
        }

        // === Druid ===
        DruidDataSource druid = new DruidDataSource();
        druid.setUrl(url);
        druid.setUsername(USER);
        druid.setPassword(PASSWORD);
        druid.setMaxActive(5);

        try (Connection conn = druid.getConnection()) {
            assertNotNull(conn);
            System.out.println("Druid 连接类: " + conn.getClass().getName());
        } finally {
            druid.close();
        }

        System.out.println("两种连接池均实现 javax.sql.DataSource,业务代码无需关心底层实现");
    }

如何选择连接池

  • HikariCP:性能最优,Spring Boot 默认,大多数场景首选
  • Druid:功能丰富,需要 SQL 监控/慢查询统计/连接池状态监控时首选

HikariCP 基本配置

HikariCP 是目前 Java 生态中性能最优的连接池实现,也是 Spring Boot 的默认连接池。

HikariCP 连接池创建与基本参数配置
    /**
     * 演示创建 HikariConfig + HikariDataSource,配置连接池参数
     */
    @Test
    void testHikariBasicConfig() throws SQLException {
        String url = "jdbc:h2:mem:testdb_pool_basic;DB_CLOSE_DELAY=-1";
        // 创建 HikariCP 配置
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(url);
        config.setUsername(USER);
        config.setPassword(PASSWORD);
        // 连接池核心参数
        config.setMaximumPoolSize(10);  // 最大连接数
        config.setMinimumIdle(2);       // 最小空闲连接数
        config.setConnectionTimeout(30000); // 连接超时(毫秒)
        config.setIdleTimeout(600000);      // 空闲连接超时(毫秒)

        // 通过配置创建数据源(连接池)
        try (HikariDataSource dataSource = new HikariDataSource(config)) {
            System.out.println("HikariCP 连接池已创建");
            System.out.println("最大连接数: " + config.getMaximumPoolSize());
            System.out.println("最小空闲: " + config.getMinimumIdle());

            // 从连接池获取连接并执行查询
            try (Connection conn = dataSource.getConnection();
                 Statement stmt = conn.createStatement()) {

                assertNotNull(conn, "从连接池获取的连接不应为 null");
                assertTrue(conn.isValid(2), "连接应有效");

                // 执行简单查询验证连接可用
                ResultSet rs = stmt.executeQuery("SELECT 1 AS result");
                assertTrue(rs.next());
                assertEquals(1, rs.getInt("result"), "查询结果应为 1");
                System.out.println("连接池连接测试成功,查询结果: " + rs.getInt("result"));
            }
        }
    }

📊 对比与最佳实践

连接池 vs 直连对比

DriverManager 直连 vs HikariCP 连接池使用方式对比
    /**
     * 对比直接 DriverManager 和连接池的使用方式
     */
    @Test
    void testPoolVsDirect() throws SQLException {
        String url = "jdbc:h2:mem:testdb_pool_vs;DB_CLOSE_DELAY=-1";

        // === 方式1:DriverManager 直接获取连接 ===
        // 每次都创建新的物理连接,开销大
        // 连接关闭后不可复用,需要重新创建
        try (Connection directConn = DriverManager.getConnection(url, USER, PASSWORD)) {
            assertNotNull(directConn);
            System.out.println("DriverManager 直连: " + directConn.getClass().getName());
            // 注意:close() 后连接被销毁,无法复用
        }

        // === 方式2:HikariCP 连接池 ===
        // 预先创建连接放入池中,获取时直接从池中取出
        // 归还时连接不会销毁,而是放回池中等待复用
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(url);
        config.setUsername(USER);
        config.setPassword(PASSWORD);
        config.setMaximumPoolSize(5);
        config.setMinimumIdle(1);

        try (HikariDataSource pool = new HikariDataSource(config)) {
            try (Connection poolConn = pool.getConnection()) {
                assertNotNull(poolConn);
                System.out.println("连接池连接: " + poolConn.getClass().getName());
                // 注意:close() 不是销毁连接,而是归还到池中
                // 性能优势:
                // 1. 避免频繁创建/销毁物理连接的开销
                // 2. 连接复用,减少数据库服务器压力
                // 3. 可控制最大连接数,防止连接耗尽
            }
        }
    }

DataSource 标准接口

DataSourcejavax.sql 包中的标准接口,生产代码应面向此接口编程,而非直接依赖 HikariDataSource

通过 DataSource 接口获取连接,连接用后自动归还池
    /**
     * 演示从 DataSource 获取连接的标准方式
     * 通过 try-with-resources 自动归还连接到池中
     */
    @Test
    void testDataSourceGetConnection() throws SQLException {
        String url = "jdbc:h2:mem:testdb_pool_ds;DB_CLOSE_DELAY=-1";
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(url);
        config.setUsername(USER);
        config.setPassword(PASSWORD);
        config.setMaximumPoolSize(5);
        config.setMinimumIdle(1);

        try (HikariDataSource dataSource = new HikariDataSource(config)) {
            // DataSource 是 JDBC 标准接口,HikariDataSource 是其实现
            DataSource ds = dataSource;

            // 第一次获取连接
            try (Connection conn1 = ds.getConnection()) {
                assertNotNull(conn1);
                System.out.println("第1次获取连接: " + conn1);
                System.out.println("连接类: " + conn1.getClass().getName());

                // 创建测试表
                try (Statement stmt = conn1.createStatement()) {
                    stmt.execute("CREATE TABLE IF NOT EXISTS pool_test (id INT, name VARCHAR(50))");
                    stmt.executeUpdate("INSERT INTO pool_test VALUES (1, '池化连接测试')");
                }
            }
            // try-with-resources 结束时 conn1.close() 被自动调用
            // 但连接不会被销毁,而是归还到连接池中等待复用

            // 第二次获取连接(可能复用了之前归还的连接)
            try (Connection conn2 = ds.getConnection()) {
                assertNotNull(conn2);
                System.out.println("第2次获取连接: " + conn2);

                // 验证之前的数据仍然存在(共享同一个内存数据库)
                try (Statement stmt = conn2.createStatement();
                     ResultSet rs = stmt.executeQuery("SELECT * FROM pool_test WHERE id = 1")) {
                    assertTrue(rs.next(), "应能读取到之前插入的数据");
                    assertEquals("池化连接测试", rs.getString("name"));
                    System.out.println("通过连接池第2次连接验证数据: " + rs.getString("name"));
                }

                // 清理
                try (Statement stmt = conn2.createStatement()) {
                    stmt.execute("DROP TABLE IF EXISTS pool_test");
                }
            }
        }
    }