连接池优化:数据库连接打满的排查与解决

一、连接池打满的惨烈现场

2025年某电商平台大促,凌晨1点告警爆发:

  • 200个数据库连接全部占满,新请求无法获取连接
  • 接口超时率飙升至60%,下单流程全线瘫痪
  • 应用日志疯狂刷Connection is not available, request timed out after 30000ms

紧急扩容数据库连接数到400,短暂缓解后30分钟再次打满——问题不是连接不够,是连接没被释放。

根因排查

  • 慢查询阻塞连接:3条SQL执行超过10秒,占住连接不放
  • 事务过长:下单事务包含4次RPC调用,平均耗时3秒
  • 连接泄漏:5个代码路径没有关闭连接
  • 连接池配置不合理:maxLifetime=30分钟,连接频繁重建

这不是个案。连接池问题是数据库相关的最高频故障类型。


二、连接池原理与配置

2.1 连接池工作原理

连接池核心流程:

应用请求连接 → 池中有空闲连接?→ 是 → 分配连接 → 执行SQL → 归还池
                               → 否 → 等待(timeout)→ 超时 → 报错

连接池内部状态:
┌────────────────────────────────────────┐
│  Total = MaxPoolSize (如200)            │
│  Active = 当前正在使用的连接数           │
│  Idle   = 空闲等待分配的连接数           │
│                                         │
│  Active + Idle = Total                  │
│                                         │
│  当Active = Total时:                    │
│  新请求进入等待队列                       │
│  等待超过connectionTimeout则报错          │
└────────────────────────────────────────┘

2.2 HikariCP核心配置

HikariCP是当前性能最好的JDBC连接池(Spring Boot默认)。

# HikariCP推荐配置
spring:
  datasource:
    hikari:
      # 核心参数
      maximum-pool-size: 20          # 最大连接数(重要!见下方计算公式)
      minimum-idle: 5                # 最小空闲连接数
      connection-timeout: 30000      # 获取连接超时时间(30秒)

      # 连接生命周期
      max-lifetime: 1800000          # 连接最大存活时间(30分钟)
      idle-timeout: 600000           # 空闲连接超时时间(10分钟)
      keepalive-time: 60000          # 保活探测间隔(60秒)

      # 连接验证
      connection-test-query: SELECT 1  # 连接有效性检查(MySQL)

      # 泄漏检测
      leak-detection-threshold: 60000  # 连接泄漏检测阈值(60秒)

2.3 最大连接数计算公式

连接数计算公式:

maximumPoolSize = (core_count * 2) + effective_spindle_count

示例:
- 8核CPU + 1个磁盘(MySQL实例) → 8*2 + 1 = 17
- 8核CPU + 2个磁盘(MySQL主从读写分离) → 8*2 + 2 = 18

简化公式:大多数场景 maximumPoolSize = 10-20 就够了

关键认知:
- 连接数不是越多越好
- 连接数过多会导致MySQL上下文切换开销增大
- MySQL每个连接占用约1MB内存
- 20个连接 vs 200个连接:在8核机器上,吞吐量差异<10%

经验值:
- 小型应用(QPS<1000):10-15
- 中型应用(QPS 1000-5000):20-30
- 大型应用(QPS>5000):30-50(配合读写分离)

连接数与吞吐量的关系

连接数QPS平均延迟MySQL CPU
1080012ms30%
20150013ms45%
50200015ms55%
100210025ms80%
200190050ms95%(上下文切换开销)

结论:连接数超过50后,吞吐量反而下降。


三、连接打满的排查流程

3.1 标准排查步骤

连接打满排查五步法:

Step 1: 确认连接池状态
  → 查HikariCP指标:active/idle/waiting连接数
  → 确认是否真的打满

Step 2: 查MySQL端连接
  → SHOW PROCESSLIST:看每个连接在干什么
  → 重点关注:Time列大的(慢查询)、State列异常的

Step 3: 分析慢查询
  → 查MySQL慢查询日志
  → 定位执行时间长的SQL

Step 4: 分析连接持有时间
  → 查应用端连接持有时间分布
  → 开启leak-detection-threshold检测泄漏

Step 5: 分析业务逻辑
  → 事务是否过长?
  → 是否有批量操作占住连接?
  → 是否有未关闭的连接?

3.2 排查命令

# 1. 查看连接池状态(Spring Boot Actuator)
curl http://localhost:8080/actuator/metrics/hikaricp.connections.active
curl http://localhost:8080/actuator/metrics/hikaricp.connections.idle
curl http://localhost:8080/actuator/metrics/hikaricp.connections.pending

# 2. 查看MySQL连接状态
mysql -e "SHOW PROCESSLIST" | wc -l      # 连接总数
mysql -e "SHOW PROCESSLIST" | grep Sleep  # 空闲连接
mysql -e "SHOW PROCESSLIST" | grep Query  # 执行查询的连接

# 3. 查看长时间占用的连接
mysql -e "SELECT * FROM information_schema.processlist WHERE Time > 5 ORDER BY Time DESC"

# 4. 查看慢查询
mysql -e "SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 20"

# 5. HikariCP泄漏检测日志
grep "Connection leak detection" /var/log/app.log

3.3 MySQL连接状态分析

-- 查看当前连接详细状态
SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, INFO
FROM information_schema.PROCESSLIST
WHERE TIME > 5  -- 超过5秒的连接
ORDER BY TIME DESC;

-- 常见STATE含义:
-- Sending data     → 正在执行查询(可能是慢查询)
-- Waiting for lock → 等待锁释放(锁冲突)
-- Sleep            → 空闲等待(连接池中的空闲连接)
-- Statistics       → 正在计算统计信息

-- 查看连接来源分布
SELECT SUBSTRING_INDEX(HOST, ':', 1) AS source_ip, COUNT(*) AS conn_count
FROM information_schema.PROCESSLIST
GROUP BY source_ip
ORDER BY conn_count DESC;

四、连接池优化策略

4.1 慢查询治理

// 慢查询是连接打满的首要原因
// 解决方案:SQL优化 + 索引优化 + 查询拆分

// 1. 紧急措施:设置SQL执行超时
@Configuration
public class DataSourceConfig {

    @Bean
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        // ... 基础配置 ...

        // 设置连接获取超时(防止等待过久)
        config.setConnectionTimeout(30000);  // 30秒

        // 设置查询超时(防止慢查询阻塞连接)
        config.addDataSourceProperty("queryTimeout", 10);  // 10秒

        return new HikariDataSource(config);
    }
}

// 2. 慢查询SQL优化示例
// 原始慢查询(全表扫描,3秒)
@Query("SELECT * FROM orders WHERE status = ? AND create_time > ?")
List<Order> findOrders(String status, Date since);

// 优化:添加索引 + 限制返回数量
// 索引:CREATE INDEX idx_status_time ON orders(status, create_time)
@Query("SELECT id, order_no, amount FROM orders WHERE status = ? AND create_time > ? LIMIT 100")
List<Order> findOrdersLimited(String status, Date since);

4.2 事务精简

// 事务过长是连接打满的第二大原因

// 错误:在事务中做RPC调用(连接被占3秒+)
@Transactional
public OrderResult createOrder(OrderRequest req) {
    // 1. DB操作:创建订单(5ms)
    orderRepo.save(order);

    // 2. RPC调用:查询库存(200ms)← 不该在事务中!
    inventoryService.checkStock(req.getSkuId());

    // 3. RPC调用:预扣款(300ms)← 不该在事务中!
    paymentService.prePay(req.getUserId(), req.getAmount());

    // 4. DB操作:更新状态(5ms)
    orderRepo.updateStatus(orderId, "PRE_PAY");

    // 事务持有连接 = 5ms + 200ms + 300ms + 5ms = 510ms
    // 应该只持有5ms + 5ms = 10ms
}

// 修复:事务只包含DB操作,RPC调用移到事务外
public OrderResult createOrderFixed(OrderRequest req) {
    // 1. 事务外:RPC调用(不占连接)
    inventoryService.checkStock(req.getSkuId());
    paymentService.prePay(req.getUserId(), req.getAmount());

    // 2. 事务内:只做DB操作(连接持有10ms)
    return doInTransaction(() -> {
        orderRepo.save(order);
        orderRepo.updateStatus(orderId, "PRE_PAY");
        return new OrderResult(orderId);
    });
}

4.3 连接泄漏检测与修复

// 开启HikariCP泄漏检测
// leak-detection-threshold = 60000(60秒未归还视为泄漏)

// 常见泄漏场景1:手动获取连接未关闭
public void leakyCode() {
    Connection conn = dataSource.getConnection();  // 获取连接
    // ... 执行SQL ...
    // 缺少 conn.close()  → 连接泄漏!
}

// 修复:使用try-with-resources
public void fixedCode() {
    try (Connection conn = dataSource.getConnection()) {
        // ... 执行SQL ...
    }  // 自动关闭连接
}

// 常见泄漏场景2:Spring事务未提交
@Transactional
public void leakyTransaction() {
    orderRepo.save(order);
    throwException();  // 异常后事务回滚,但某些情况下连接未归还
}

// 修复:确保事务方法有完善的异常处理
@Transactional(rollbackFor = Exception.class)
public void fixedTransaction() {
    try {
        orderRepo.save(order);
    } catch (Exception e) {
        log.error("Order creation failed", e);
        throw e;  // 确保触发事务回滚
    }
}

4.4 读写分离降低单库压力

// 读写分离:写操作走主库,读操作走从库
// 连接池压力从200降到100(主库+从库各100)

@Configuration
public classReadWriteSplitConfig {

    @Bean("masterDataSource")
    public DataSource masterDataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://master:3306/db");
        config.setMaximumPoolSize(15);   // 写操作连接数较少
        // ... 其他配置 ...
        return new HikariDataSource(config);
    }

    @Bean("slaveDataSource")
    public DataSource slaveDataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://slave:3306/db");
        config.setMaximumPoolSize(20);   // 读操作连接数较多
        config.setReadOnly(true);         // 标记只读
        // ... 其他配置 ...
        return new HikariDataSource(config);
    }
}

// 动态路由:根据方法自动选择数据源
@Component
public class DynamicDataSourceRouter extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        // 读方法走从库,写方法走主库
        return TransactionSynchronizationManager.isCurrentTransactionReadOnly()
            ? "slave" : "master";
    }
}

五、连接池监控体系

5.1 HikariCP指标监控

// 注册HikariCP指标到Prometheus
@Component
public class HikariCPMetrics {

    @Autowired
    private HikariDataSource dataSource;

    @Autowired
    private MeterRegistry meterRegistry;

    public void registerMetrics() {
        HikariPoolMXBean pool = dataSource.getHikariPoolMXBean();

        // 核心指标
        Gauge.builder("hikaricp.connections.active", pool, HikariPoolMXBean::getActiveConnections)
            .description("Active connections")
            .register(meterRegistry);

        Gauge.builder("hikaricp.connections.idle", pool, HikariPoolMXBean::getIdleConnections)
            .description("Idle connections")
            .register(meterRegistry);

        Gauge.builder("hikaricp.connections.pending", pool, HikariPoolMXBean::getThreadsAwaitingConnection)
            .description("Pending connection requests")
            .register(meterRegistry);

        Gauge.builder("hikaricp.connections.total", pool, HikariPoolMXBean::getTotalConnections)
            .description("Total connections")
            .register(meterRegistry);
    }
}

5.2 告警规则

# Prometheus告警规则
groups:
  - name: hikaricp
    rules:
      # 连接池使用率超过80%
      - alert: HikariCPConnectionsHigh
        expr: hikaricp_connections_active / hikaricp_connections_total > 0.8
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "连接池使用率超过80%"

      # 等待连接的线程数超过5
      - alert: HikariCPConnectionsPending
        expr: hikaricp_connections_pending > 5
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "有{{ $value }}个线程等待数据库连接"

      # 连接泄漏检测
      - alert: HikariCPLeakDetected
        expr: increase(hikaricp_connections_leak[5m]) > 0
        labels:
          severity: critical

六、踩坑总结

坑点1:连接池大小=数据库max_connections

问题:应用HikariCP最大200连接,MySQL max_connections=200,其他应用/监控工具无法连接。

解决:MySQL max_connections应大于所有应用连接池总和 + 管理连接预留。

MySQL max_connections = Σ(各应用maximumPoolSize) + 50(预留)

示例:3个应用(20+20+15) + 50 = 105

坑点2:maxLifetime与MySQL wait_timeout不匹配

问题:HikariCP maxLifetime=30分钟,MySQL wait_timeout=8小时,连接在MySQL端已失效但HikariCP不知道。

解决:HikariCP maxLifetime应比MySQL wait_timeout小几秒。

# MySQL wait_timeout=28800秒(8小时)
# HikariCP maxLifetime应设置为 < 28800秒
hikari:
  max-lifetime: 1740000   # 29分钟(比wait_timeout小很多,安全)

坑点3:连接验证开销

问题:connection-test-query=SELECT 1,每次获取连接都执行一次,高并发时开销可观。

解决:HikariCP默认使用Connection.isValid()(JDBC4+),不需要test-query。删除connection-test-query配置。

坑点4:自动提交误关闭

问题:手动获取Connection后设置autoCommit=false,归还池时未恢复autoCommit=true,后续使用该连接的事务行为异常。

解决:HikariCP默认在归还连接时重置autoCommit,确保不要手动覆盖这个行为。


七、总结

连接池打满的本质是连接持有时间过长——要么是慢查询阻塞,要么是事务过长,要么是连接泄漏。

核心要点

  1. 连接数不是越多越好,10-20足以应对大多数场景
  2. 慢查询是连接打满的首要原因——治理慢查询是根本
  3. 事务中不要做RPC调用——事务只包含DB操作
  4. 连接泄漏检测阈值必须开启——60秒未归还视为泄漏
  5. maxLifetime必须小于MySQL wait_timeout
  6. 读写分离是降低单库连接压力的有效手段

排查口诀:看池状态 → 查MySQL连接 → 找慢查询 → 测连接持有时间 → 查泄漏日志。


作者:架构实战团队
日期:2026-07-17
标签:#连接池 #HikariCP #数据库 #慢查询 #连接泄漏

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐