【架构实战】连接池优化:数据库连接打满的排查与解决
连接池优化:数据库连接打满的排查与解决
一、连接池打满的惨烈现场
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)curlhttp://localhost:8080/actuator/metrics/hikaricp.connections.activecurlhttp://localhost:8080/actuator/metrics/hikaricp.connections.idlecurlhttp://localhost:8080/actuator/metrics/hikaricp.connections.pending# 2. 查看MySQL连接状态mysql-e"SHOW PROCESSLIST"|wc-l# 连接总数mysql-e"SHOW PROCESSLIST"|grepSleep# 空闲连接mysql-e"SHOW PROCESSLIST"|grepQuery# 执行查询的连接# 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.log3.3 MySQL连接状态分析
-- 查看当前连接详细状态SELECTID,USER,HOST,DB,COMMAND,TIME,STATE,INFOFROMinformation_schema.PROCESSLISTWHERETIME>5-- 超过5秒的连接ORDERBYTIMEDESC;-- 常见STATE含义:-- Sending data → 正在执行查询(可能是慢查询)-- Waiting for lock → 等待锁释放(锁冲突)-- Sleep → 空闲等待(连接池中的空闲连接)-- Statistics → 正在计算统计信息-- 查看连接来源分布SELECTSUBSTRING_INDEX(HOST,':',1)ASsource_ip,COUNT(*)ASconn_countFROMinformation_schema.PROCESSLISTGROUPBYsource_ipORDERBYconn_countDESC;四、连接池优化策略
4.1 慢查询治理
// 慢查询是连接打满的首要原因// 解决方案:SQL优化 + 索引优化 + 查询拆分// 1. 紧急措施:设置SQL执行超时@ConfigurationpublicclassDataSourceConfig{@BeanpublicDataSourcedataSource(){HikariConfigconfig=newHikariConfig();// ... 基础配置 ...// 设置连接获取超时(防止等待过久)config.setConnectionTimeout(30000);// 30秒// 设置查询超时(防止慢查询阻塞连接)config.addDataSourceProperty("queryTimeout",10);// 10秒returnnewHikariDataSource(config);}}// 2. 慢查询SQL优化示例// 原始慢查询(全表扫描,3秒)@Query("SELECT * FROM orders WHERE status = ? AND create_time > ?")List<Order>findOrders(Stringstatus,Datesince);// 优化:添加索引 + 限制返回数量// 索引: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(Stringstatus,Datesince);4.2 事务精简
// 事务过长是连接打满的第二大原因// 错误:在事务中做RPC调用(连接被占3秒+)@TransactionalpublicOrderResultcreateOrder(OrderRequestreq){// 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调用移到事务外publicOrderResultcreateOrderFixed(OrderRequestreq){// 1. 事务外:RPC调用(不占连接)inventoryService.checkStock(req.getSkuId());paymentService.prePay(req.getUserId(),req.getAmount());// 2. 事务内:只做DB操作(连接持有10ms)returndoInTransaction(()->{orderRepo.save(order);orderRepo.updateStatus(orderId,"PRE_PAY");returnnewOrderResult(orderId);});}4.3 连接泄漏检测与修复
// 开启HikariCP泄漏检测// leak-detection-threshold = 60000(60秒未归还视为泄漏)// 常见泄漏场景1:手动获取连接未关闭publicvoidleakyCode(){Connectionconn=dataSource.getConnection();// 获取连接// ... 执行SQL ...// 缺少 conn.close() → 连接泄漏!}// 修复:使用try-with-resourcespublicvoidfixedCode(){try(Connectionconn=dataSource.getConnection()){// ... 执行SQL ...}// 自动关闭连接}// 常见泄漏场景2:Spring事务未提交@TransactionalpublicvoidleakyTransaction(){orderRepo.save(order);throwException();// 异常后事务回滚,但某些情况下连接未归还}// 修复:确保事务方法有完善的异常处理@Transactional(rollbackFor=Exception.class)publicvoidfixedTransaction(){try{orderRepo.save(order);}catch(Exceptione){log.error("Order creation failed",e);throwe;// 确保触发事务回滚}}4.4 读写分离降低单库压力
// 读写分离:写操作走主库,读操作走从库// 连接池压力从200降到100(主库+从库各100)@ConfigurationpublicclassReadWriteSplitConfig{@Bean("masterDataSource")publicDataSourcemasterDataSource(){HikariConfigconfig=newHikariConfig();config.setJdbcUrl("jdbc:mysql://master:3306/db");config.setMaximumPoolSize(15);// 写操作连接数较少// ... 其他配置 ...returnnewHikariDataSource(config);}@Bean("slaveDataSource")publicDataSourceslaveDataSource(){HikariConfigconfig=newHikariConfig();config.setJdbcUrl("jdbc:mysql://slave:3306/db");config.setMaximumPoolSize(20);// 读操作连接数较多config.setReadOnly(true);// 标记只读// ... 其他配置 ...returnnewHikariDataSource(config);}}// 动态路由:根据方法自动选择数据源@ComponentpublicclassDynamicDataSourceRouterextendsAbstractRoutingDataSource{@OverrideprotectedObjectdetermineCurrentLookupKey(){// 读方法走从库,写方法走主库returnTransactionSynchronizationManager.isCurrentTransactionReadOnly()?"slave":"master";}}五、连接池监控体系
5.1 HikariCP指标监控
// 注册HikariCP指标到Prometheus@ComponentpublicclassHikariCPMetrics{@AutowiredprivateHikariDataSourcedataSource;@AutowiredprivateMeterRegistrymeterRegistry;publicvoidregisterMetrics(){HikariPoolMXBeanpool=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:hikaricprules:# 连接池使用率超过80%-alert:HikariCPConnectionsHighexpr:hikaricp_connections_active / hikaricp_connections_total>0.8for:5mlabels:severity:warningannotations:summary:"连接池使用率超过80%"# 等待连接的线程数超过5-alert:HikariCPConnectionsPendingexpr:hikaricp_connections_pending>5for:2mlabels:severity:criticalannotations:summary:"有{{ $value }}个线程等待数据库连接"# 连接泄漏检测-alert:HikariCPLeakDetectedexpr:increase(hikaricp_connections_leak[5m])>0labels: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 #数据库 #慢查询 #连接泄漏