知识模块
☕ Java 知识模块
十二、性能优化
连接池优化

连接池优化

面试高频考点:连接池原理、HikariCP 配置、Druid 配置、参数调优策略

一、连接池基础

为什么需要连接池?

没有连接池时

每次数据库操作:
1. TCP 三次握手建立连接(~10ms)
2. MySQL 认证(~5ms)
3. 执行 SQL(~1ms)
4. 关闭连接(~5ms)
5. TCP 四次挥手(~10ms)

总耗时:~31ms,实际业务只占 1ms

使用连接池后

应用启动时:
1. 预创建 N 个连接

每次数据库操作:
1. 从池中获取连接(<1ms)
2. 执行 SQL(~1ms)
3. 归还连接到池(<1ms)

总耗时:~3ms,提升 10 倍

连接池核心原理

┌─────────────────────────────────────────────────────────────┐
│                        连接池                                │
│  ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐                   │
│  │连接1│ │连接2│ │连接3│ │连接4│ │连接5│  ← 活跃连接池       │
│  └─────┘ └─────┘ └─────┘ └─────┘ └─────┘                   │
│                                                             │
│  应用请求连接 → 池中有空闲连接? → 是 → 返回连接             │
│                      ↓                                      │
│                      否                                     │
│                      ↓                                      │
│              连接数 < 最大值? → 是 → 创建新连接              │
│                      ↓                                      │
│                      否                                     │
│                      ↓                                      │
│              等待超时 → 抛出异常                             │
└─────────────────────────────────────────────────────────────┘

连接池核心参数

参数说明推荐值
minimumIdle最小空闲连接数与 maximumPoolSize 相同
maximumPoolSize最大连接数CPU 核数 * 2 + 有效磁盘数
idleTimeout空闲连接超时时间600000ms (10分钟)
maxLifetime连接最大存活时间1800000ms (30分钟)
connectionTimeout获取连接超时时间30000ms (30秒)
connectionTestQuery连接测试 SQLSELECT 1

二、HikariCP 连接池

HikariCP 是 Spring Boot 2.x 默认的连接池,以高性能著称。

为什么 HikariCP 快?

1. 字节码级别优化
   - 使用 Javassist 生成代理类
   - 减少方法调用层级

2. 数据结构优化
   - ConcurrentBag 替代 BlockingQueue
   - 无锁设计,减少竞争

3. 代理优化
   - 直接代理 Connection,不包装 Statement
   - 减少对象创建

4. 连接验证优化
   - 使用 JDBC4 的 isValid() 方法
   - 避免执行测试 SQL

HikariCP 配置

Spring Boot 配置

# application.yml
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: password
    type: com.zaxxer.hikari.HikariDataSource
    hikari:
      # 连接池名称
      pool-name: MyHikariPool
      
      # 连接数配置(核心)
      minimum-idle: 10                    # 最小空闲连接
      maximum-pool-size: 20               # 最大连接数
      
      # 超时配置
      idle-timeout: 600000                # 空闲超时 10 分钟
      max-lifetime: 1800000               # 最大存活 30 分钟
      connection-timeout: 30000           # 获取超时 30 秒
      
      # 连接验证
      connection-test-query: SELECT 1     # 测试 SQL(JDBC4 可不配)
      validation-timeout: 3000            # 验证超时 3 秒
      
      # 泄漏检测
      leak-detection-threshold: 60000     # 泄漏检测阈值 60 秒
      
      # 只读配置
      read-only: false
      
      # 注册 MBean(JMX 监控)
      register-mbeans: true

Java 配置

@Configuration
public class DataSourceConfig {
    
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.hikari")
    public DataSource dataSource() {
        return DataSourceBuilder.create()
                .type(HikariDataSource.class)
                .build();
    }
    
    // 或手动配置
    @Bean
    public DataSource hikariDataSource() {
        HikariConfig config = new HikariConfig();
        config.setDriverClassName("com.mysql.cj.jdbc.Driver");
        config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
        config.setUsername("root");
        config.setPassword("password");
        
        // 连接池配置
        config.setMinimumIdle(10);
        config.setMaximumPoolSize(20);
        config.setIdleTimeout(600000);
        config.setMaxLifetime(1800000);
        config.setConnectionTimeout(30000);
        config.setPoolName("MyHikariPool");
        
        // 性能优化
        config.addDataSourceProperty("cachePrepStmts", "true");
        config.addDataSourceProperty("prepStmtCacheSize", "250");
        config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
        config.addDataSourceProperty("useServerPrepStmts", "true");
        config.addDataSourceProperty("useLocalSessionState", "true");
        config.addDataSourceProperty("rewriteBatchedStatements", "true");
        config.addDataSourceProperty("cacheResultSetMetadata", "true");
        config.addDataSourceProperty("cacheServerConfiguration", "true");
        config.addDataSourceProperty("elideSetAutoCommits", "true");
        config.addDataSourceProperty("maintainTimeStats", "false");
        
        return new HikariDataSource(config);
    }
}

HikariCP 监控

@RestController
public class PoolController {
    
    @Autowired
    private DataSource dataSource;
    
    @GetMapping("/pool/stats")
    public Map<String, Object> poolStats() {
        HikariDataSource ds = (HikariDataSource) dataSource;
        HikariPoolMXBean pool = ds.getHikariPoolMXBean();
        
        Map<String, Object> stats = new HashMap<>();
        stats.put("activeConnections", pool.getActiveConnections());
        stats.put("idleConnections", pool.getIdleConnections());
        stats.put("totalConnections", pool.getTotalConnections());
        stats.put("threadsAwaitingConnection", pool.getThreadsAwaitingConnection());
        return stats;
    }
}

三、Druid 连接池

Druid 是阿里巴巴开源的连接池,监控功能丰富。

Druid 配置

Spring Boot 配置

# application.yml
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: password
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      # 连接池配置
      initial-size: 10                    # 初始连接数
      min-idle: 10                        # 最小空闲连接
      max-active: 20                      # 最大连接数
      max-wait: 60000                     # 获取连接超时
      
      # 连接检测
      validation-query: SELECT 1          # 测试 SQL
      test-while-idle: true               # 空闲时检测
      test-on-borrow: false               # 获取时检测(影响性能)
      test-on-return: false               # 归还时检测(影响性能)
      
      # 空闲连接检测
      time-between-eviction-runs-millis: 60000  # 检测间隔
      min-evictable-idle-time-millis: 300000    # 最小空闲时间
      
      # 连接保活
      keep-alive: true                    # 保持连接活跃
      phy-max-use-count: 1000             # 物理连接最大使用次数
      
      # 监控配置
      stat-view-servlet:
        enabled: true
        url-pattern: /druid/*
        login-username: admin
        login-password: admin
        reset-enable: false
      
      # Web 监控
      web-stat-filter:
        enabled: true
        url-pattern: /*
        exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
      
      # SQL 监控
      filter:
        stat:
          enabled: true
          log-slow-sql: true              # 记录慢 SQL
          slow-sql-millis: 3000           # 慢 SQL 阈值
          merge-sql: true
        wall:
          enabled: true                   # 防火墙
          config:
            multi-statement-allow: true

Java 配置

@Configuration
public class DruidConfig {
    
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.druid")
    public DataSource druidDataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        
        // 基本配置
        dataSource.setUrl("jdbc:mysql://localhost:3306/mydb");
        dataSource.setUsername("root");
        dataSource.setPassword("password");
        
        // 连接池配置
        dataSource.setInitialSize(10);
        dataSource.setMinIdle(10);
        dataSource.setMaxActive(20);
        dataSource.setMaxWait(60000);
        
        // 连接检测
        dataSource.setValidationQuery("SELECT 1");
        dataSource.setTestWhileIdle(true);
        dataSource.setTestOnBorrow(false);
        dataSource.setTestOnReturn(false);
        
        // 空闲检测
        dataSource.setTimeBetweenEvictionRunsMillis(60000);
        dataSource.setMinEvictableIdleTimeMillis(300000);
        
        // 开启监控
        try {
            dataSource.setFilters("stat,wall");
        } catch (SQLException e) {
            e.printStackTrace();
        }
        
        return dataSource;
    }
    
    // 配置监控页面
    @Bean
    public ServletRegistrationBean<StatViewServlet> druidStatViewServlet() {
        ServletRegistrationBean<StatViewServlet> bean = 
            new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
        bean.addInitParameter("loginUsername", "admin");
        bean.addInitParameter("loginPassword", "admin");
        bean.addInitParameter("resetEnable", "false");
        return bean;
    }
    
    // 配置 Web 监控
    @Bean
    public FilterRegistrationBean<WebStatFilter> druidWebStatFilter() {
        FilterRegistrationBean<WebStatFilter> bean = 
            new FilterRegistrationBean<>(new WebStatFilter());
        bean.addUrlPatterns("/*");
        bean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
        return bean;
    }
}

Druid 监控功能

访问 http://localhost:8080/druid/ 可以看到:

1. 数据源信息
   - 活跃连接数
   - 空闲连接数
   - 连接池配置

2. SQL 监控
   - 执行次数
   - 执行时间
   - 最慢 SQL
   - 错误次数

3. SQL 防火墙
   - 危险 SQL 拦截
   - SQL 统计

4. Web 应用监控
   - URI 监控
   - Session 监控
   - Spring 监控

四、HikariCP vs Druid

特性HikariCPDruid
性能最快较快
监控基础(JMX)丰富(Web UI)
SQL 监控
慢 SQL 记录
SQL 防火墙
配置复杂度简单复杂
社区活跃度高(阿里维护)
Spring Boot 默认

选择建议

  • 追求性能:HikariCP
  • 需要监控:Druid
  • 生产环境推荐:HikariCP + Prometheus + Grafana

五、连接数计算公式

最大连接数计算

最大连接数 = (CPU 核数 * 2) + 有效磁盘数

解释:
- CPU 核数 * 2:充分利用 CPU(线程数)
- 有效磁盘数:并发 I/O 能力

示例:
- 8 核 CPU,1 块磁盘:最大连接数 = 8 * 2 + 1 = 17
- 16 核 CPU,RAID 10(4 块盘):最大连接数 = 16 * 2 + 4 = 36

注意:这是理论值,实际需要根据数据库服务器配置和负载调整。

连接数评估方法

// 监控连接池使用情况,获取实际需要的连接数
@RestController
public class PoolMonitor {
    
    @GetMapping("/pool/recommend")
    public String recommendPoolSize() {
        // 1. 观察高峰期活跃连接数
        // 2. maximumPoolSize = 高峰活跃数 * 1.5
        // 3. minimumIdle = 平均活跃数
        
        // 示例:高峰期活跃 15,平均活跃 8
        // maximumPoolSize = 15 * 1.5 = 22
        // minimumIdle = 8
        
        return "Observe active connections during peak hours";
    }
}

六、连接泄漏排查

现象

- 连接池连接数逐渐增加
- 最终达到最大值,无法获取新连接
- 应用报错:Connection is not available

HikariCP 泄漏检测

spring:
  datasource:
    hikari:
      leak-detection-threshold: 60000  # 60 秒未归还视为泄漏

日志输出

Apparent connection leak detected. Connection has been out of pool for 60000ms.

常见泄漏原因

// 问题 1:忘记关闭连接
public void badMethod() throws SQLException {
    Connection conn = dataSource.getConnection();
    // 业务逻辑...
    // 忘记 conn.close()
}
 
// 修复:使用 try-with-resources
public void goodMethod() throws SQLException {
    try (Connection conn = dataSource.getConnection()) {
        // 业务逻辑...
    }
}
 
// 问题 2:异常时未关闭
public void badMethod() throws SQLException {
    Connection conn = dataSource.getConnection();
    Statement stmt = conn.createStatement();
    stmt.execute("...");  // 这里抛异常,连接未关闭
    conn.close();
}
 
// 修复
public void goodMethod() throws SQLException {
    try (Connection conn = dataSource.getConnection();
         Statement stmt = conn.createStatement()) {
        stmt.execute("...");
    }
}
 
// 问题 3:事务未提交/回滚
@Transactional
public void badMethod() {
    // 业务逻辑...
    throw new RuntimeException();  // 事务未正确处理
}
 
// 修复:确保事务正确管理
@Transactional(rollbackFor = Exception.class)
public void goodMethod() {
    // 业务逻辑...
}

七、连接池监控指标

关键监控指标

1. 活跃连接数 (active_connections)
   - 当前正在使用的连接数
   - 持续接近最大值需扩容

2. 空闲连接数 (idle_connections)
   - 当前空闲的连接数
   - 持续为 0 需扩容

3. 等待获取连接的线程数 (threads_awaiting)
   - 等待连接的请求数
   - 大于 0 说明连接池不足

4. 获取连接平均时间 (connection_acquire_time)
   - 从请求到获取连接的时间
   - 过高说明连接池压力大

5. 连接使用时间 (connection_usage_time)
   - 连接被占用的平均时间
   - 过高可能有慢 SQL

Prometheus + Grafana 监控

# pom.xml
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
 
# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus
  metrics:
    export:
      prometheus:
        enabled: true

访问指标http://localhost:8080/actuator/prometheus

关键指标

# 活跃连接数
hikaricp_connections_active{pool="MyHikariPool"}

# 空闲连接数
hikaricp_connections_idle{pool="MyHikariPool"}

# 最大连接数
hikaricp_connections_max{pool="MyHikariPool"}

# 获取连接时间
hikaricp_connections_creation_seconds_sum

八、面试要点

Q1: 为什么需要连接池?

回答要点

  1. 避免频繁创建/销毁连接的开销(TCP 连接、MySQL 认证)
  2. 复用连接,提高性能
  3. 控制并发连接数,保护数据库
  4. 提供连接监控和管理功能

Q2: HikariCP 为什么快?

回答要点

  1. 字节码级别优化(Javassist 生成代理类)
  2. ConcurrentBag 无锁设计
  3. 直接代理 Connection,减少对象创建
  4. 使用 JDBC4 isValid() 避免测试 SQL

Q3: 如何确定连接池大小?

回答要点

// 公式
maximumPoolSize = CPU核数 * 2 + 有效磁盘数
 
// 实际评估
1. 观察高峰期活跃连接数
2. maximumPoolSize = 高峰活跃数 * 1.5
3. minimumIdle = 平均活跃数
4. 根据监控数据持续优化

Q4: 连接泄漏如何排查?

回答要点

  1. 开启泄漏检测:leak-detection-threshold
  2. 使用 try-with-resources 确保连接关闭
  3. 检查事务是否正确提交/回滚
  4. 使用 Druid 监控查看活跃连接

Q5: HikariCP 和 Druid 如何选择?

回答要点

场景选择
追求极致性能HikariCP
需要丰富监控Druid
需要慢 SQL 分析Druid
Spring Boot 项目HikariCP(默认)

小结

  • 连接池复用连接,避免频繁创建/销毁
  • HikariCP 性能最优,Spring Boot 默认
  • Druid 监控丰富,适合需要 SQL 分析场景
  • 连接数公式:CPU核数 * 2 + 有效磁盘数
  • 开启泄漏检测,使用 try-with-resources
  • 监控关键指标:活跃/空闲连接数、等待线程数