连接池失效——高并发下的隐形杀手
系统挂了
现象:用户打开页面,一直转圈。5分钟后,页面报错。
错误日志:
org.apache.tomcat.jdbc.pool.PoolExhaustedException: [http-nio-8080-exec-72] Timeout: Pool empty. Unable to fetch a connection in 30 seconds, none available[size:100; busy:100; idle:0; lastwait:30000]诊断:连接池满了。100个连接全部被占用,没有空闲连接可用。
这不是第一次,也不是最后一次。
连接池是什么
连接池是数据库连接的"蓄水池":
应用线程 → 从连接池取连接 → 执行SQL → 归还连接到池 ↓ ┌──────────────┐ │ 连接池 │ │ ┌──┐┌──┐┌──┐│ │ │c1││c2││c3││ ← 预先创建好的数据库连接 │ └──┘└──┘└──┘│ └──────────────┘正常情况:线程用完连接就归还,连接池循环利用。
故障情况:线程拿了连接不归还,连接池逐渐枯竭。
故障案例
案例1:数据库重启,连接池"假死"
处理因数据库重启而失效的中间件连接池场景:Oracle 数据库例行重启后,应用无法访问数据库。
排查:
- 数据库已正常启动
- 应用服务器已重启
- 但应用还是报连接超时
根因:连接池中的连接是数据库重启前创建的。数据库重启后,这些连接已经失效(TCP 连接断开)。但连接池不知道连接已失效,继续把坏连接分配给应用。
解决方案:
<!-- Tomcat JDBC Pool 配置 --><Resourcename="jdbc/datasource"auth="Container"type="javax.sql.DataSource"factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"maxActive="100"maxIdle="30"minIdle="10"initialSize="10"<!--关键配置:连接有效性检查-->validationQuery="SELECT 1 FROM DUAL" validationQueryTimeout="5" testOnBorrow="true"<!-- 取连接时检查 -->testWhileIdle="true"<!-- 空闲时检查 -->timeBetweenEvictionRunsMillis="30000" minEvictableIdleTimeMillis="60000"<!-- 数据库重启后自动重建连接 -->removeAbandoned="true" removeAbandonedTimeout="60" logAbandoned="true" />教训:连接池必须配置连接有效性检查,否则数据库重启就是一场灾难。
案例2:连接池泄露——代码忘了归还
异地平台连接池故障分析及处理(注释掉连接放入容器中) 异地平台连接池泄露问题解决与测试现象:系统运行一段时间后变慢,最终崩溃。
排查:
# 1. 查看连接数netstat-an|grep1521|grepESTABLISHED|wc-l# 结果:200+# 2. 查看应用连接池状态(JMX)# busy: 100, idle: 0根因:代码中获取连接后,在异常分支没有归还。
// 错误代码:连接泄露publicvoidprocessData(){Connectionconn=null;try{conn=dataSource.getConnection();// 业务处理...// 这里抛出异常thrownewBusinessException("处理失败");}catch(Exceptione){// 只记录日志,没有归还连接!log.error("处理失败",e);// 缺少:conn.close();}}修复:
// 正确代码:try-with-resources 自动归还publicvoidprocessData(){try(Connectionconn=dataSource.getConnection()){// 业务处理...}catch(Exceptione){log.error("处理失败",e);}// 自动归还连接!}排查技巧:开启removeAbandoned和logAbandoned,定位泄露点。
removeAbandoned=true removeAbandonedTimeout=60 logAbandoned=true启用后日志会打印:
WARNING: Connection has been abandoned. PooledConnection[x.x.x.x:1521] StackTrace: com.xxx.dao.DataProcessor.processData(DataProcessor.java:45) com.xxx.service.DataService.handle(DataService.java:23) ...案例3:一体化系统连接池跟踪
一体化跟踪(连接池泄露) 一体化数据连接泄露跟踪现象:一体化系统运行几天后,连接池耗尽。
排查过程:
- 开启连接池监控
- 记录每天的连接使用情况
- 对比正常时段和异常时段
// 连接池监控代码@ComponentpublicclassConnectionPoolMonitor{@Scheduled(fixedRate=60000)// 每分钟记录一次publicvoidmonitor(){DataSourceds=getDataSource();if(dsinstanceoforg.apache.tomcat.jdbc.pool.DataSource){org.apache.tomcat.jdbc.pool.DataSourcetomcatDS=(org.apache.tomcat.jdbc.pool.DataSource)ds;PoolStatsstats=newPoolStats();stats.setActive(tomcatDS.getActive());stats.setIdle(tomcatDS.getIdle());stats.setSize(tomcatDS.getSize());stats.setWaitCount(tomcatDS.getWaitCount());stats.setTimestamp(newDate());log.info("连接池状态: active={}, idle={}, size={}, wait={}",stats.getActive(),stats.getIdle(),stats.getSize(),stats.getWaitCount());// 如果活跃连接数超过阈值,告警if(stats.getActive()>tomcatDS.getMaxActive()*0.8){alertService.sendAlert("连接池告警",stats.toString());}}}}发现:某个定时任务在周末数据量大时执行慢,连接占用时间长。
修复:
// 优化:增加超时控制@Scheduled(cron="0 0 2 * * ?")publicvoidbatchProcess(){// 1. 限制单次处理数量intbatchSize=1000;// 2. 分批处理,每批之间释放连接for(inti=0;i<totalCount;i+=batchSize){try{processBatch(i,batchSize);}catch(Exceptione){log.error("批次处理失败",e);// 失败后继续下一批,而不是一直占用连接}// 每批完成后短暂休眠,让其他线程有机会获取连接Thread.sleep(100);}}案例4:MyBatis 连接池管理问题
一体化问题处理(连接占完) 一体化系统性能分析(推测是mybatis连接池管理问题)现象:连接池满了,但代码看起来没问题(都用了 try-with-resources)。
根因:项目用的 MyBatis 自带连接池(POOLED),poolMaximumActiveConnections只配了10个,高峰期远远不够。同时嵌套事务的传播行为配置不当,导致连接占用时间过长。
排查过程——先判断是池太小还是泄漏:
| 原因 | 特征 | 排查方式 |
|---|---|---|
| 连接池太小 | 高峰期定时耗尽,重启后正常,负载下来后也正常 | 调大连接池观察 |
| 连接泄漏 | 越来越严重,即使低峰也耗尽 | 检查代码是否忘关连接 |
临时把poolMaximumActiveConnections从 10 调到 30 观察一周,依然耗尽——确认是连接泄漏。
代码泄漏点定位:
// 嫌疑一:手动获取Connection后没有在finally中关闭Connectionconn=session.getConnection();PreparedStatementps=conn.prepareStatement(sql);ResultSetrs=ps.executeQuery();// rs、ps、conn 都没有关闭!// 嫌疑二:事务未正常提交/回滚DBUtil.BeginTrans(false);// 如果中间抛异常,EndTrans没调用,连接不归还// 嫌疑三:大数据导出ResultSet流式读取未关闭ResultSetresult=DBUtil.getBigResult(session,mapperClass,methodName,params);// 导出完没关result,Statement和Connection都占着修复方案:
- 导出逻辑的 finally 块中确保关闭 ResultSet 和 Statement
- 封装 try-with-resources 安全获取连接的方法
- BeginTrans/EndTrans 增加30秒超时保护,超时写入 warn 日志
MyBatis POOLED vs 生产级连接池:
| 功能 | MyBatis POOLED | Druid/HikariCP |
|---|---|---|
| 连接泄漏检测 | 无 | 有(强制回收超时连接) |
| 慢SQL记录 | 无 | 有 |
| 连接池监控 | 无 | 有(JMX/SQL面板) |
| 动态调整 | 不支持 | 支持 |
长期方案:换用 Druid/HikariCP,
removeAbandoned参数可自动回收泄漏连接。
解决:调大连接池、调整事务传播行为、缩短事务范围。同时在导出逻辑 finally 中确保关闭 ResultSet,BeginTrans/EndTrans 加超时告警,封装 try-with-resources 安全方法。
案例5:MongoDB 连接池
mongodb连接池修改(改为连接集群,使用配置文件)场景:MongoDB 从单机升级到集群,连接池配置需要调整。
// MongoDB 连接池配置MongoClientOptionsoptions=MongoClientOptions.builder().connectionsPerHost(100)// 每个主机的最大连接数.minConnectionsPerHost(10)// 最小连接数.threadsAllowedToBlockForConnectionMultiplier(5).connectTimeout(5000)// 连接超时.socketTimeout(30000)// Socket超时.maxWaitTime(10000)// 最大等待时间.build();MongoClientclient=newMongoClient(Arrays.asList(newServerAddress("192.168.1.1",27017),newServerAddress("192.168.1.2",27017),newServerAddress("192.168.1.3",27017)),options);连接池配置大全
Tomcat JDBC Pool
<Resourcename="jdbc/datasource"type="javax.sql.DataSource"factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"maxActive="100"<!--最大活跃连接数-->maxIdle="30"<!-- 最大空闲连接数 -->minIdle="10"<!-- 最小空闲连接数 -->initialSize="10"<!-- 初始连接数 -->maxWait="30000"<!-- 最大等待时间(ms) -->validationQuery="SELECT 1 FROM DUAL" validationQueryTimeout="5" testOnBorrow="true" testOnReturn="false" testWhileIdle="true" timeBetweenEvictionRunsMillis="30000" minEvictableIdleTimeMillis="60000" removeAbandoned="true" removeAbandonedTimeout="60" logAbandoned="true" jdbcInterceptors="ConnectionState;StatementFinalizer;SlowQueryReport(threshold=1000)" />Druid Pool
<beanid="dataSource"class="com.alibaba.druid.pool.DruidDataSource"><propertyname="url"value="jdbc:oracle:thin:@localhost:1521:orcl"/><propertyname="username"value="user"/><propertyname="password"value="pass"/><propertyname="initialSize"value="10"/><propertyname="minIdle"value="10"/><propertyname="maxActive"value="100"/><propertyname="maxWait"value="30000"/><propertyname="validationQuery"value="SELECT 1 FROM DUAL"/><propertyname="testOnBorrow"value="true"/><propertyname="testWhileIdle"value="true"/><propertyname="timeBetweenEvictionRunsMillis"value="60000"/><propertyname="removeAbandoned"value="true"/><propertyname="removeAbandonedTimeout"value="60"/><propertyname="logAbandoned"value="true"/><!-- 慢SQL监控 --><propertyname="filters"value="stat,wall,log4j"/></bean>HikariCP(推荐)
spring:datasource:hikari:maximum-pool-size:50minimum-idle:10idle-timeout:300000max-lifetime:600000connection-timeout:30000connection-test-query:SELECT 1 FROM DUALleak-detection-threshold:60000诊断工具
1. 查看数据库连接数
-- Oracle 查看当前连接SELECTusername,count(*)FROMv$sessionWHEREusernameISNOTNULLGROUPBYusername;-- 查看具体连接信息SELECTsid,serial#, username, machine, program, statusFROMv$sessionWHEREusername='APP_USER'ORDERBYstatus;2. 查看应用连接池(JMX)
# 通过 JMX 查看 Tomcat 连接池jconsole localhost:1099# → MBeans → Catalina → DataSource → connectionPool3. 查看线程堆栈
# 找出哪些线程在等待连接jstack<pid>|grep-A20"POOL EXHAUSTED"jstack<pid>|grep-A20"borrowConnection"4. 数据库级排查
-- Oracle 查看锁SELECTobject_name,session_id,oracle_username,os_user_name,locked_modeFROMv$locked_object lo,dba_objectsdoWHERElo.object_id=do.object_id;-- 查看长时间运行的SQLSELECTsid,serial#, username,ROUND(elapsed_seconds/60,2)asminutes,sql_textFROMv$sessions,v$sqlqWHEREs.sql_id=q.sql_idANDs.status='ACTIVE'ANDs.usernameISNOTNULLORDERBYelapsed_secondsDESC;常见原因总结
| 原因 | 表现 | 解决方案 |
|---|---|---|
| 代码未归还连接 | 活跃连接数持续上升 | try-with-resources |
| 事务过长 | 连接占用时间长 | 缩小事务范围 |
| 慢SQL | 连接执行慢,排队 | 优化SQL、加索引 |
| 数据库重启 | 连接全部失效 | testOnBorrow |
| 并发突增 | 连接不够用 | 增大连接池、限流 |
| 连接泄露 | 活跃连接慢慢增加 | removeAbandoned |
| 死锁 | 连接互相等待 | 优化锁顺序 |
| 网络抖动 | 连接假死 | 连接超时检测 |
经验教训
1. 连接池不是越大越好
maxActive=100 满了 → 改成 200 还是满了 → 改成 500 数据库扛不住了连接池越大,数据库压力越大。根本问题是:为什么连接占用时间这么长?
2. 一定要配置连接有效性检查
testOnBorrow=true validationQuery=SELECT 1 FROM DUAL没有这个配置,数据库重启一次,系统就要重启一次。
3. 一定要开启泄露检测
removeAbandoned=true logAbandoned=true生产环境必须开启,否则连接泄露只有到系统崩溃时才发现。
4. 监控比预防更重要
连接池问题很难在测试环境复现(并发不够)。生产环境必须监控:
- 活跃连接数
- 等待连接数
- 连接获取时间
- 连接关闭时间
5. 慢SQL是连接池的头号杀手
一条慢 SQL 执行 10 秒,如果有 100 个连接,只能同时处理 10 个请求,剩下的 90 个排队。
先优化慢 SQL,再调整连接池。
最后的话
连接池问题是"隐形杀手"——平时不出问题,一出就是大问题。
日志里多次出现的"连接池泄露"、“连接占完”、“连接池故障分析”,每次都是系统快挂了才发现。
事后分析原因都很简单:代码少了个close(),事务范围太大了,SQL 太慢了。但查出来往往要花半天。
最有效的方案是:
- 代码层面:try-with-resources(Java 7+ 的自动资源管理)
- 配置层面:testOnBorrow + removeAbandoned
- 监控层面:连接池指标实时监控,超过 80% 就告警