
之前在公司内部群里收到一条消息“残虹姐刚才外边人多卡池的事拜托了”乍一看以为在聊游戏抽卡但残虹姐是我们组负责订单库的 DBA她提到的“卡池”显然不是什么抽卡池而是服务端最常见的“数据库连接池”。当时正好碰上订单服务频繁告警接口耗时从 50ms 涨到 3s连接池监控面板上的活跃连接数直接打满。我顺着这次排查过程整理了一篇完整笔记从连接池原理、参数配置到踩坑复现、根因定位希望能帮到正在被连接池问题困扰的同学。1. 数据库连接池到底是什么1.1 为什么需要连接池在讲连接池之前先看一个最简单的场景。每次业务请求需要操作数据库时应用都会做下面这几件事创建数据库连接TCP 三次握手、MySQL 认证、权限校验。执行 SQL。关闭连接发送关闭请求、释放服务端会话资源。在低并发场景下这套流程没什么问题。但一旦请求量上来每次新建连接的成本就会非常可观。这里有一个很直观的对比数据一次典型的 MySQL 连接建立过程网络开销加上服务端会话初始化耗时大概在 20ms 到 100ms 之间具体取决于网络环境和数据库负载。而一条主键查询 SQL 本身可能只需要 1ms。也就是说如果每次请求都新建连接连接建立时间可能占整个请求耗时的 90% 以上。这就是连接池存在的意义提前创建一批连接放在池子里应用程序需要时从池中“借”用完“还”回去由池统一管理和维护连接的生命周期。1.2 连接池的工作流程连接池的核心逻辑可以拆成四步初始化连接池启动时按配置创建一定数量的空闲连接。借用borrow业务线程需要数据库操作时从池中获取一个连接。归还return业务操作完成后连接归还到池中变成空闲状态。回收与补充池会定期检查空闲连接是否超时、是否被数据库主动断开如果连接无效就丢弃并补充新连接。从应用开发者的视角看连接池就是在DataSource.getConnection()和connection.close()之间做了一层透明的代理。你写的代码可能感知不到连接池的存在但性能、稳定性、并发能力都受它制约。1.3 主流连接池怎么选Java 生态常见的连接池有这三个连接池特点适用场景HikariCP轻量、性能高、Spring Boot 2.x/3.x 默认绝大多数 Spring Boot 项目Druid功能丰富自带监控面板、SQL 防火墙需要可视化监控、团队习惯阿里生态Tomcat JDBC PoolTomcat 容器内置配置简单纯 Tomcat 部署且不想额外引入依赖本文的实战场景基于 HikariCP因为它是 Spring Boot 的默认选择不需要额外配置就能跑起来最适合用来演示连接池问题。Druid 的参数思路和排错思路也大同小异。2. 环境准备与版本说明连接池本身是中间件层面的东西不挑具体业务但为了完整复现问题我们需要一个可运行的 Spring Boot 项目。本文示例环境如下操作系统Windows / macOS / Linux 均可。JDK8 或 11 或 17。Spring Boot2.x如果使用 Spring Boot 3.x需要 JDK 17。数据库MySQL 8.x。数据库连接池HikariCPSpring Boot 内置默认。构建工具Maven 3.6。IDEIntelliJ IDEA 或 Eclipse。需要注意JDK、Spring Boot、MySQL 的具体版本号请以你本机环境为准。本文重点是连接池的配置与排查思路不同小版本之间差异不影响整体流程。下面是示例项目的目录结构connection-pool-demo ├── pom.xml ├── src │ └── main │ ├── java │ │ └── com │ │ └── example │ │ └── pool │ │ ├── ConnectionPoolDemoApplication.java │ │ ├── controller │ │ │ └── UserController.java │ │ ├── mapper │ │ │ └── UserMapper.java │ │ └── service │ │ └── UserService.java │ └── resources │ └── application.yml3. HikariCP 核心参数与配置拆解连接池的参数很多但真正需要开发者重点关注的就那七八个。下面逐个拆解。3.1 核心容量参数maximumPoolSizemaximumPoolSize表示连接池允许的最大连接数。当池中没有空闲连接且当前连接数小于该值时连接池会继续创建新连接。这里有一个很多新手容易误解的点maximumPoolSize不是越大越好。连接数越多数据库服务端需要维护的会话就越多MySQL 默认最大连接数通常是 151max_connections如果应用层把连接池设成 500数据库可能直接拒绝连接。HikariCP 官方文档中有一个经验公式连接数 ((核心线程数 * 2) 有效磁盘数)但这个公式只适合“单机、低响应时间”的粗略估算。实际项目中更合理的做法是从业务 TPS 和 RT 反推连接数 预期 TPS × (单次请求数据库耗时(秒) / 1000)举个例子接口预期 TPS 是 500每次请求平均占用数据库连接的时间是 20ms那么并发需要的连接数大约是 500 × 0.02 10 个。考虑到毛刺和慢查询再留一部分余量。minimumIdleminimumIdle表示连接池中保持的最少空闲连接数。当空闲连接数低于该值时连接池会尝试补充新连接直到达到minimumIdle或maximumPoolSize。如果minimumIdle和maximumPoolSize设置相同相当于池大小固定请求量大时不会临时扩容减少连接创建开销但空闲时也占用数据库连接资源。如果两者差异较大则属于“按需增长”模式。HikariCP 官方建议如果对性能有较高要求推荐将两者设置为相同值这样可以避免突发流量下的连接创建延迟。3.2 超时类参数connectionTimeoutconnectionTimeout是客户端从连接池获取连接的最大等待时间默认 30000ms30 秒。如果等待超过该时间仍然拿不到连接会抛出类似下面的异常Connection is not available, request timed out after 30000ms.这个参数在生产环境建议适当调小比如 3000ms 或 5000ms。因为如果 3 秒都拿不到连接说明连接池要么太小要么存在连接泄漏继续等下去只会拖垮整个请求链路的响应时间。validationTimeoutvalidationTimeout用于连接有效性检测的超时时间默认 5000ms。连接池在借出连接前如果开启isValid检查会执行一次探活 SQL 或调用 JDBC 的isValid()方法。3.3 连接生命周期参数idleTimeoutidleTimeout是空闲连接存活时间默认 600000ms10 分钟。只有当minimumIdle小于maximumPoolSize时这个参数才会生效。空闲连接超过该时间会被回收直到空闲连接数降到minimumIdle。maxLifetimemaxLifetime是连接的最大存活时间默认 1800000ms30 分钟。这个参数非常重要因为很多数据库比如 MySQL会主动断开空闲超过一定时间的连接。如果连接池不知道连接已被数据库断开业务拿到“死连接”后执行 SQL 就会报错。建议maxLifetime设置成比数据库wait_timeout略小。MySQL 默认wait_timeout是 8 小时所以 HikariCP 的 30 分钟默认值一般够用。3.4 连接泄漏检测参数leakDetectionThresholdleakDetectionThreshold是连接泄漏检测阈值默认 0不启用。如果设置为 2000表示连接借出超过 2000ms 未归还时连接池会输出一条警告日志提示可能存在连接泄漏。建议在测试环境和低流量环境开启该参数帮助提前发现未正常关闭连接的问题。生产环境由于性能损耗可以按需开启。3.5 完整配置示例下面是一份比较完整的 HikariCP 配置放在 Spring Boot 的application.yml中spring: datasource: url: jdbc:mysql://localhost:3306/demo_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver hikari: # 池中最小空闲连接数 minimum-idle: 10 # 池中最大连接数 maximum-pool-size: 20 # 获取连接超时时间(ms) connection-timeout: 3000 # 连接闲置超时时间(ms) idle-timeout: 600000 # 连接最大存活时间(ms) max-lifetime: 1800000 # 连接泄漏检测阈值(ms)0表示不开启 leak-detection-threshold: 5000 # 连接有效性检测超时时间(ms) validation-timeout: 3000这里解释几个细节driver-class-name在 Spring Boot 2.x 配合 MySQL 8.x 时应写成com.mysql.cj.jdbc.Driver。serverTimezoneAsia/Shanghai是为了避免时区问题导致的日期报错。useSSLfalse是在本机测试环境下关闭 SSL 加密生产环境请根据数据库配置决定是否开启。如果使用 Druid参数名不同比如initial-size、max-active、min-idle但调优思路是一样的。4. 完整实战案例一次连接池耗尽的排查全过程下面回到开头的场景。订单服务突然告警接口平均耗时从 50ms 涨到 3s数据库 CPU 没有明显飙升但连接池监控显示活跃连接数一直在上限附近徘徊。4.1 事故现象线上最先暴露出来的现象有三类接口响应时间大幅上涨部分请求直接超时。应用日志中出现HikariPool-1 - Connection is not available, request timed out after 3000ms.。数据库慢查询日志中出现了几条执行时间很长的 SQL但数量并不多。这类现象组合在一起基本可以初步断定是连接池被占满而不是数据库本身慢。4.2 初步排查应用日志首先看应用日志。下面是一段典型的 HikariCP 连接池耗尽日志2025-06-20 14:23:11.018 ERROR [http-nio-8080-exec-12] c.e.pool.controller.UserController : 查询用户失败 java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 3000ms. at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:696) at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:196) ...出现这个异常说明业务线程在getConnection()阶段等待了 3 秒还是没有获取到连接。接下来需要区分两种情况连接池容量确实不够。连接被占用但一直没归还泄漏。怎么区分看数据库侧的SHOW PROCESSLIST或监控工具。4.3 数据库侧排查活跃会话分布登录 MySQL 执行SHOW PROCESSLIST;如果看到大量连接处于Sleep状态但应用侧连接池又提示没有可用连接说明这些连接是被业务代码“借走”后没有及时归还或关闭。更精确的分析可以查询information_schema中的连接信息SELECT user, host, db, command, time, state, info FROM information_schema.processlist ORDER BY time DESC LIMIT 20;重点看command列和time列。如果有很多Query状态的连接长时间不结束可能是慢 SQL如果有大量Sleep连接且time很大可能是连接泄漏。4.4 复现实验模拟连接泄漏为了还原问题我在本地写了一个模拟未归还连接的接口。假设有一个UserService其中某个方法查询数据库后没有关闭连接同时模拟了较慢的 SQL代码如下// 文件路径src/main/java/com/example/pool/service/UserService.java package com.example.pool.service; import org.springframework.stereotype.Service; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.Statement; Service public class UserService { /** * 模拟一个存在连接泄漏的查询 * 手动获取 Connection但没有在 finally 中关闭。 */ public String queryUserLeak(Long userId) throws Exception { // 这里直接通过 DriverManager 获取连接模拟从连接池拿到连接后未归还 Connection connection getConnectionFromPool(); try { PreparedStatement ps connection.prepareStatement( SELECT * FROM user WHERE id ? ); ps.setLong(1, userId); ResultSet rs ps.executeQuery(); // 模拟业务处理耗时拉长连接占用时间 Thread.sleep(200); if (rs.next()) { return rs.getString(name); } return null; } finally { // 注意这里没有关闭 connection // ps 和 rs 也没有关闭 } } /** * 模拟从连接池获取连接的方法。 * 实际项目中这里应该是 DataSource.getConnection()。 */ private Connection getConnectionFromPool() throws Exception { return springDataSource().getConnection(); } private javax.sql.DataSource springDataSource() { // 实际项目中由 Spring 注入这里仅为演示 return SpringContextHolder.getBean(javax.sql.DataSource.class); } }这个代码的核心问题在finally块中缺少connection.close()。在连接池场景下调用connection.close()实际上不会真正断开数据库连接而是把连接归还给连接池。如果这里不调用close()连接就会一直被业务线程持有直到连接池达到maximumPoolSize。为了模拟方便我不推荐你真的在业务代码中写这种有问题的代码这里只是为了复现问题。实际项目中更常见的泄漏原因是在try块中获取了连接SQL 抛异常后没有走finally关闭。4.5 定位根因慢 SQL 与未关闭连接在我这次模拟中真正压垮连接池的是三层因素叠加接口中有一个Sleep(200)模拟慢业务处理导致连接占用时间变长。连接没有在finally中释放即使处理完成连接也不会回到池中。并发请求一旦达到maximumPoolSize后续所有请求都会阻塞在getConnection()等待阶段。这三层因素叠加的最终结果就是连接池被占满所有新请求超时。4.6 修复与验证修复方案分两步。第一步是修复代码层面的连接泄漏。强烈推荐使用 Java 7 引入的 try-with-resources 语法它能保证连接、语句、结果集在代码块结束后自动关闭// 文件路径src/main/java/com/example/pool/service/UserService.java package com.example.pool.service; import org.springframework.stereotype.Service; import javax.sql.DataSource; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; Service public class UserService { private final DataSource dataSource; public UserService(DataSource dataSource) { this.dataSource dataSource; } public String queryUserCorrect(Long userId) throws Exception { String sql SELECT * FROM user WHERE id ?; // try-with-resources 自动关闭 Connection、PreparedStatement、ResultSet try (Connection connection dataSource.getConnection(); PreparedStatement ps connection.prepareStatement(sql)) { ps.setLong(1, userId); try (ResultSet rs ps.executeQuery()) { // 模拟业务处理耗时 Thread.sleep(200); if (rs.next()) { return rs.getString(name); } return null; } } } }注意这里的Connection是连接池返回的代理对象connection.close()不代表物理断开它只是把连接标记为“归还到池中”。所以即使业务代码里写了close()也无需担心真的关掉连接导致连接池失效。第二步是检查连接池参数是否合理。在我这次的案例中maximumPoolSize只有 10但接口 QPS 在高峰期超过 300单次请求占用连接 200ms 以上算下来需要的连接数约为 60 个配置明显偏小。调整配置并增加连接泄漏检测后问题得到解决。调整后的配置片段spring: datasource: hikari: minimum-idle: 20 maximum-pool-size: 50 connection-timeout: 3000 leak-detection-threshold: 2000 max-lifetime: 1800000验证时可以在测试环境用压测工具如 JMeter、wrk模拟并发请求观察以下指标接口 RT响应时间是否恢复。活跃连接数是否稳定在合理范围。日志中是否还出现Connection is not available。应用进程是否还出现大量Sleep状态的数据库连接。5. 常见问题与排查思路这一节把连接池最常见的几种问题整理成表格方便大家遇到问题时快速定位。问题现象常见原因排查步骤解决思路Connection is not available, request timed out after 3000ms连接池被占满查看活跃连接数、数据库 processlist调大 maximumPoolSize排查连接泄漏降低单请求连接占用时间大量连接处于Sleep状态连接未归还查询 processlist统计 Sleep 连接来源 IP 和应用检查是否缺少 close开启 leakDetectionThreshold使用 try-with-resources偶发 SQL 执行报错Communications link failure数据库主动断开空闲连接查看 MySQLwait_timeout与连接池maxLifetime将maxLifetime设置为小于数据库wait_timeout启动时初始化慢连接池在启动阶段创建连接查看网络、数据库负载合理设置minimumIdle避免启动时大量创建连接连接池大小设置过大数据库连接数被打满查看 MySQLmax_connections检查业务并发量将连接池大小和数据库连接上限对齐5.1 连接池被占满的排查清单如果你在线上遇到连接池问题可以按下面的顺序快速排查先看应用日志确认是不是 HikariPool 超时异常。再看连接池监控确认活跃连接数和等待获取连接的线程数。进入数据库执行SHOW PROCESSLIST确认连接是否存在泄漏。检查最近是否上线了慢 SQL 或不规范代码没加finally、手动开了事务没提交。压测验证修复效果不要只改参数不查代码。5.2 要不要调大连接池很多同学遇到连接池超时的第一反应是调大maximumPoolSize但这并不总是正确。数据库的连接数是有限资源。每个连接在 MySQL 端都会占用一定的内存会话缓冲区、排序缓冲区等。连接数越多数据库侧的内存开销越大同时锁竞争和上下文切换也会变多最终可能导致数据库整体性能下降。更合理的做法是先排查代码是否有连接泄漏。再优化慢 SQL缩短连接占用时间。然后结合压测数据调整连接池大小。最后通过监控持续观察。6. 最佳实践与工程建议6.1 代码层面统一使用 try-with-resources只要是手动操作 JDBC就应该使用 try-with-resources 来自动关闭连接。即使代码中抛出异常连接也会被正确归还。// 推荐写法自动关闭 try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { // 执行 SQL }如果项目中使用 MyBatis 或 JPA这些框架已经帮我们处理了连接释放但要注意自定义拦截器、TypeHandler 中如果手动获取了连接仍然要负责释放。6.2 参数配置先理解业务再调整参数连接池参数没有绝对的“标准答案”但可以遵循下面几个原则maximumPoolSize不宜超过数据库实例可承受的连接上限建议给其他应用或运维工具预留连接。connectionTimeout建议设置 3~5 秒不要让请求无限等待。leakDetectionThreshold在测试环境开启帮助提前发现泄漏。不要让maxLifetime大于数据库wait_timeout否则会拿到被数据库断开后的“死连接”。6.3 监控告警连接池指标必须纳入监控连接池和 CPU、内存一样是应用健康度的核心指标。建议至少监控以下指标active-threads活跃线程数。idle-connections空闲连接数。pending-connections等待获取连接的请求数。total-connections总连接数。time-to-acquire-connection获取连接耗时。有条件的话开启 HikariCP 的 Metrics。Spring Boot Actuator 中可以通过/actuator/metrics/hikaricp.connections.active等端点获取连接池指标curl http://localhost:8080/actuator/metrics/hikaricp.connections.active如果项目使用 Druid则可以开启 Druid 的 Web Stat 和监控面板用可视化方式查看连接池使用情况。6.4 上线前压测提前压出问题连接池问题往往在低流量下很难暴露等到大促或流量高峰才出现。所以上线前一定要做并发压测至少覆盖正常并发量验证连接池参数是否合理。超过预期的峰值流量观察连接池是否被打满。慢 SQL 场景观察慢查询对连接池的占用情况。压测时重点记录 TPS、RT、连接池活跃连接数、数据库连接数这些数据能为后续调优提供依据。6.5 事务中的连接释放这里要特别提醒一个容易踩的坑事务场景下连接释放不是在close()时归还而是在事务提交或回滚后才归还。Service public class OrderService { Transactional public void createOrder(Order order) { // 1. 插入订单 // 2. 扣减库存 // 3. 记录日志 } }如果方法内部某一步执行很慢连接会一直被事务持有。此时连接池的活跃连接数会持续上升但线程可能并不在getConnection()等待。所以遇到连接池打满也要关注是否有长事务。-- 查看当前执行时间较长的事务 SELECT * FROM information_schema.innodb_trx ORDER BY trx_started ASC LIMIT 10;6.6 生产环境变更注意事项修改连接池参数属于生产变更建议遵循以下流程在测试环境完成压测验证。通过配置中心或发布系统灰度发布不要所有实例一次性变更。变更后观察连接池监控曲线和业务错误率。保留回滚方案。7. 总结与下一步回到开头那句“残虹姐刚才外边人多卡池的事拜托了”这里的“卡池”其实就是数据库连接池。连接池看起来是配置文件中几行参数但一旦出现问题可能导致整个服务不可用。本文从连接池的原理出发梳理了 HikariCP 的核心参数并通过一个连接泄漏的模拟场景完整演示了从现象到根因再到修复的排查过程。最后整理了常见问题排查清单和工程最佳实践。接下来你可以继续学习这几个方向数据库慢 SQL 优化连接池只是入口真正的问题是 SQL 执行效率。Spring 事务传播机制理解事务与连接生命周期之间的关系。压测工具的使用JMeter、wrk、Gatling用数据指导参数调优。如果你也被连接池问题困扰过建议先从SHOW PROCESSLIST开始排查再结合监控面板确定是容量问题还是代码问题。排查过一次之后你就不会再被“卡池”难住了。