数据库连接池折腾手记
先复现,再谈优化。
很多人一上来就讲数据库连接池的全景图;我更想先把这次卡住的点说清楚。
问题的开始
线上环境是 Java Spring Boot 应用,MySQL 8.0,用的是默认的 HikariCP。初期流量不大的时候一切正常,但随着业务增长,开始出现偶发的超时。
最开始以为是不是数据库有问题,检查了慢查询、索引、锁等待,都没什么异常。直到有一次把连接池监控打开了,才发现问题的端倪。
# 查看当前数据库连接数
mysql> SHOW STATUS LIKE 'Threads_connected';
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Threads_connected | 487 |
+-------------------+-------+
# 查看最大连接数配置
mysql> SHOW VARIABLES LIKE 'max_connections';
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| max_connections | 500 |
+-----------------+-------+
连接数已经快到上限了,但应用实际并发并没有这么高。显然,连接没有正常回收。
连接池的基本原理
在深入排查之前,先理一下连接池的基本工作方式。
数据库连接的建立是个开销很大的操作:TCP 三次握手、MySQL 认证协议、权限校验、会话初始化,这一套下来通常要几十毫秒。如果每个请求都重新建连接,那大部分时间都花在建连接上了。
连接池的核心思路很简单:预先建立一批连接,用的时候从池子里拿,用完还回去。但这里有几个关键点需要理解:
关键在于"归还"这个动作。如果代码拿了连接但没还回去,或者还回去之前就抛了异常,连接就会泄漏。池里的连接越来越少,最终所有请求都在等连接,超时就不可避免。
另一个常见误区是以为连接池里的连接永远都是好的。实际上数据库那边的连接可能因为网络问题、超时、服务器重启等原因失效,这时候就需要连接池有检测和重建机制。
连接泄漏的排查
回到我们的问题,第一件事就是排查哪里可能有连接泄漏。
最直接的方法是开启连接池的详细日志:
# application.yml
spring:
datasource:
hikari:
leak-detection-threshold: 60000 # 60秒未归还视为泄漏
max-lifetime: 1800000 # 30分钟
connection-timeout: 30000 # 30秒获取超时
idle-timeout: 600000 # 10分钟空闲超时
maximum-pool-size: 50
minimum-idle: 10
pool-name: BusinessHikariCP
设置 leak-detection-threshold 后,HikariCP 会在检测到连接泄漏时打印堆栈信息。重新部署后,很快就定位到了问题。
问题代码大概长这样:
public void processData() {
Connection conn = dataSource.getConnection();
try {
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM large_table");
// 处理结果集
while (rs.next()) {
// 某些条件下直接返回,没有关闭连接
if (someCondition) {
return;
}
// 处理数据...
}
rs.close();
stmt.close();
} catch (SQLException e) {
log.error("数据库操作失败", e);
}
// finally 块里没有关闭连接
}
典型的资源泄漏问题。return 语句直接退出,连接没有关闭;异常处理里也没有确保连接关闭。
正确的写法应该用 try-with-resources:
public void processData() {
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM large_table")) {
while (rs.next()) {
if (someCondition) {
return; // 自动关闭所有资源
}
// 处理数据...
}
} catch (SQLException e) {
log.error("数据库操作失败", e);
}
}
修复完所有类似的代码后,连接数稳定下来了,但响应时间仍然不够理想。于是开始第二轮优化。
连接池参数调优
修复泄漏只是基础,要让连接池真正发挥作用,还需要根据实际场景调优参数。
HikariCP 的配置项不多,但每个都有它的讲究:
| 参数 | 默认值 | 说明 | 调优建议 |
|---|---|---|---|
maximum-pool-size | 10 | 最大连接数 | 根据数据库并发能力设置,通常不超过 CPU 核心数 * 2 + 有效磁盘数 |
minimum-idle | 10 | 最小空闲连接数 | 建议设置为与最大连接数相同,减少连接创建开销 |
connection-timeout | 30000 | 获取连接超时时间 | 根据业务容忍度设置,太短容易超时,太长会堆积请求 |
idle-timeout | 600000 | 空闲连接超时时间 | 10分钟左右合适,太短频繁创建连接,太长浪费资源 |
max-lifetime | 1800000 | 连接最大生命周期 | 建议比数据库 wait_timeout 稍短一些 |
leak-detection-threshold | 0 | 连接泄漏检测阈值 | 生产环境建议开启,设置 60 秒左右 |
这里有个关键点:maximum-pool-size 不是越大越好。连接数过多会导致数据库上下文切换开销增大,反而降低性能。
我们的环境配置:
spring:
datasource:
hikari:
maximum-pool-size: 32 # 8核机器,32个连接
minimum-idle: 32 # 保持所有连接常驻
connection-timeout: 10000 # 10秒超时
idle-timeout: 300000 # 5分钟
max-lifetime: 1500000 # 25分钟,小于MySQL的wait_timeout
leak-detection-threshold: 60000
validation-timeout: 3000 # 连接验证超时
connection-test-query: SELECT 1
选择 32 的原因是:我们用的是 8 核机器,每个核心处理 4 个连接是比较均衡的配置。实际测试后发现,超过 40 后性能反而开始下降。
不同连接池的对比
在优化过程中,我们也简单对比了一下几种主流连接池:
// HikariCP 配置
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/db");
config.setUsername("user");
config.setPassword("password");
config.setMaximumPoolSize(32);
HikariDataSource hikariDS = new HikariDataSource(config);
// Druid 配置
DruidDataSource druidDS = new DruidDataSource();
druidDS.setUrl("jdbc:mysql://localhost:3306/db");
druidDS.setUsername("user");
druidDS.setPassword("password");
druidDS.setMaxActive(32);
druidDS.setInitialSize(32);
druidDS.setMinIdle(32);
druidDS.setMaxWait(10000);
druidDS.setValidationQuery("SELECT 1");
druidDS.setTestWhileIdle(true);
druidDS.setTestOnBorrow(false);
druidDS.setTestOnReturn(false);
实际测试结果:
| 连接池 | 平均响应时间 | CPU 开销 | 内存占用 | 特点 |
|---|---|---|---|---|
| HikariCP | 12ms | 低 | 最小 | 轻量快速,配置简单 |
| Druid | 15ms | 中 | 较大 | 监控功能丰富,配置复杂 |
| DBCP2 | 18ms | 中 | 中 | 功能全面,但性能一般 |
| C3P0 | 25ms | 高 | 最大 | 老牌连接池,性能偏弱 |
在相同连接池大小(32)下,HikariCP 的平均响应时间明显领先——下图直观对比四款连接池的实测延迟。

HikariCP 比 C3P0 快一倍以上,且 CPU 与内存开销最低,这是我们最终选它的直接依据。
最终我们还是选择 HikariCP,原因很简单:性能好、配置简单、维护成本低。Druid 的监控功能虽然强大,但在我们的场景下,HikariCP 配合 Prometheus 已经足够。
监控和告警
连接池优化完成后,建立监控体系很重要。没有监控的优化就像闭眼开车,你不知道什么时候又会出问题。
我们用 Micrometer 集成 Prometheus 监控:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
@Configuration
public class MetricsConfig {
@Bean
public MeterRegistryCustomizer<HikariDataSource> metricsCommonTags() {
return (dataSource, registry) -> {
HikariPoolMXBean poolProxy = dataSource.getHikariPoolMXBean();
registry.gauge("hikari.active.connections", poolProxy,
p -> p.getActiveConnections());
registry.gauge("hikari.idle.connections", poolProxy,
p -> p.getIdleConnections());
registry.gauge("hikari.total.connections", poolProxy,
p -> p.getTotalConnections());
registry.gauge("hikari.threads.awaiting.connection", poolProxy,
p -> p.getThreadsAwaitingConnection());
};
}
}
关键指标:
hikari.active.connections:活跃连接数,应该稳定在一个合理范围hikari.idle.connections:空闲连接数,如果经常为 0 说明连接不够用hikari.threads.awaiting.connection:等待连接的线程数,大于 0 就要警惕hikari.total.connections:总连接数,应该接近maximum-pool-size
Prometheus 告警规则:
groups:
- name: hikari_alerts
rules:
- alert: HikariPoolNearlyFull
expr: hikari_active_connections / hikari_total_connections > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "连接池接近满载"
description: "{{ $labels.instance }} 连接池使用率超过 80%"
- alert: HikariConnectionWait
expr: hikari_threads_awaiting_connection > 5
for: 2m
labels:
severity: critical
annotations:
summary: "存在连接等待"
description: "{{ $labels.instance }} 有 {{ $value }} 个线程在等待连接"
有了监控后,我们能够及时发现异常情况。比如某次发布后,监控显示连接等待数突然上升,快速定位到是新代码里有个循环查询没有用批处理,修复后问题马上解决。
常见坑点总结
这次优化过程中,遇到过不少坑,总结一下:
1. 事务和连接池的配合
@Transactional
public void processInTransaction() {
// 这里获取的连接和事务管理的连接可能不是同一个
Connection conn = dataSource.getConnection();
// 执行一些操作...
conn.close(); // 这可能会把事务连接也关闭
}
正确做法是让 Spring 管理连接:
@Autowired
private JdbcTemplate jdbcTemplate;
@Transactional
public void processInTransaction() {
jdbcTemplate.query("SELECT * FROM table", (rs, rowNum) -> {
// 处理结果
return null;
});
}
2. 长事务占用连接
@Transactional
public void longRunningTask() {
// 先查数据
List<Data> data = repository.findAll();
// 然后做一些耗时的处理,比如调用外部API
for (Data item : data) {
externalApiCall(item); // 每次调用可能要几秒
// 数据库连接一直被占用
}
}
这种情况下,连接会被长时间占用,其他请求就拿不到连接。应该拆分事务:
public void longRunningTask() {
List<Data> data = repository.findAll(); // 事务结束后连接立即归还
for (Data item : data) {
externalApiCall(item);
repository.updateStatus(item.getId(), "processed"); // 每次更新单独事务
}
}
3. 连接池和数据库参数不匹配
MySQL 默认的 wait_timeout 是 8 小时,但如果连接池的 max-lifetime 设置得比这个长,就会出现连接失效的问题。
-- 查看 MySQL wait_timeout
SHOW VARIABLES LIKE 'wait_timeout';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| wait_timeout | 28800 | -- 8小时
+---------------+-------+
连接池的 max-lifetime 应该设置为比 wait_timeout 小,比如 25 分钟:
max-lifetime: 1500000 # 25分钟
4. 环境差异导致的问题
开发环境可能连接数少、延迟低,一切正常。到了生产环境,连接数多、网络延迟大,问题就暴露出来了。
建议在测试环境模拟生产环境的网络延迟和连接数:
# 使用 tc 模拟网络延迟
tc qdisc add dev eth0 root netem delay 100ms 20ms
优化的最终效果
经过这次完整的优化,效果还是比较明显的:
| 指标 | 优化前 | 优化后 | 改善 |
|---|---|---|---|
| 平均响应时间 | 85ms | 35ms | -59% |
| P99 响应时间 | 450ms | 120ms | -73% |
| 数据库连接数 | 450-500 | 32-35 | -92% |
| 连接等待超时 | 每小时 20+ 次 | 0 | -100% |
| CPU 使用率 | 65% | 45% | -31% |
最直观的感受是系统稳定了很多,不再出现那种莫名其妙的超时 spike。监控图表也变得平滑了,连接数稳定在 32 左右,没有异常波动。
一些实践建议
基于这次经验,给一些实践建议:
- 开发阶段就要关注连接池配置,不要等到上线了才发现问题
- 一定要开泄漏检测,
leak-detection-threshold救过我们好几次 - 监控比调优更重要,没有监控的调优是盲人摸象
- 连接数不是越大越好,根据实际硬件和并发量来设置
- 代码审查要检查资源释放,try-with-resources 能避免很多问题
- 定期检查连接池指标,把它当成系统健康检查的一部分
最后说一句
连接池看起来是个小问题,但真能影响到整个系统的稳定性。很多时候我们觉得数据库慢,其实是连接池没配置好;觉得代码写得没问题,其实是连接泄漏了。
技术这东西,很多时候就是这些"小地方"决定了最终的效果。就像这次优化,改的不是什么高大上的架构,就是几个配置参数、几处代码写法,但效果是很实在的。
这次连接池优化花了两周,从最初的超时问题到最终的稳定运行,中间踩过不少坑。现在回过头看,很多问题其实都有迹可循,只是当时没有足够的监控和经验。希望这篇记录能帮到遇到类似问题的人。
可用性说明:本文发布于 2021 年 3 月,距今已超过五年。文中涉及的软件版本、接口、下载地址、命令参数和操作界面可能已经发生变化,部分方案在当前环境下可能失效。请结合官方最新文档核对后再操作,生产环境使用前务必先行验证。
版权声明: 本文首发于 指尖魔法屋-数据库连接池折腾手记(https://blog.thinkmoon.cn/post/107-database-connection-pool-principle-optimization/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。