【架构实战】连接池优化:数据库连接打满的排查与解决
连接池优化:数据库连接打满的排查与解决
一、连接池打满的惨烈现场
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 |
|---|---|---|---|
| 10 | 800 | 12ms | 30% |
| 20 | 1500 | 13ms | 45% |
| 50 | 2000 | 15ms | 55% |
| 100 | 2100 | 25ms | 80% |
| 200 | 1900 | 50ms | 95%(上下文切换开销) |
结论:连接数超过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,确保不要手动覆盖这个行为。
七、总结
连接池打满的本质是连接持有时间过长——要么是慢查询阻塞,要么是事务过长,要么是连接泄漏。
核心要点:
- 连接数不是越多越好,10-20足以应对大多数场景
- 慢查询是连接打满的首要原因——治理慢查询是根本
- 事务中不要做RPC调用——事务只包含DB操作
- 连接泄漏检测阈值必须开启——60秒未归还视为泄漏
- maxLifetime必须小于MySQL wait_timeout
- 读写分离是降低单库连接压力的有效手段
排查口诀:看池状态 → 查MySQL连接 → 找慢查询 → 测连接持有时间 → 查泄漏日志。
作者:架构实战团队
日期:2026-07-17
标签:#连接池 #HikariCP #数据库 #慢查询 #连接泄漏
更多推荐
所有评论(0)