连接池失效——高并发下的隐形杀手
连接池失效——高并发下的隐形杀手
文章目录
系统挂了
现象:用户打开页面,一直转圈。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 配置 -->
<Resource name="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 | grep 1521 | grep ESTABLISHED | wc -l
# 结果:200+
# 2. 查看应用连接池状态(JMX)
# busy: 100, idle: 0
根因:代码中获取连接后,在异常分支没有归还。
// 错误代码:连接泄露
public void processData() {
Connection conn = null;
try {
conn = dataSource.getConnection();
// 业务处理...
// 这里抛出异常
throw new BusinessException("处理失败");
} catch (Exception e) {
// 只记录日志,没有归还连接!
log.error("处理失败", e);
// 缺少:conn.close();
}
}
修复:
// 正确代码:try-with-resources 自动归还
public void processData() {
try (Connection conn = dataSource.getConnection()) {
// 业务处理...
} catch (Exception e) {
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:一体化系统连接池跟踪
一体化跟踪(连接池泄露)
一体化数据连接泄露跟踪
现象:一体化系统运行几天后,连接池耗尽。
排查过程:
- 开启连接池监控
- 记录每天的连接使用情况
- 对比正常时段和异常时段
// 连接池监控代码
@Component
public class ConnectionPoolMonitor {
@Scheduled(fixedRate = 60000) // 每分钟记录一次
public void monitor() {
DataSource ds = getDataSource();
if (ds instanceof org.apache.tomcat.jdbc.pool.DataSource) {
org.apache.tomcat.jdbc.pool.DataSource tomcatDS =
(org.apache.tomcat.jdbc.pool.DataSource) ds;
PoolStats stats = new PoolStats();
stats.setActive(tomcatDS.getActive());
stats.setIdle(tomcatDS.getIdle());
stats.setSize(tomcatDS.getSize());
stats.setWaitCount(tomcatDS.getWaitCount());
stats.setTimestamp(new Date());
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 * * ?")
public void batchProcess() {
// 1. 限制单次处理数量
int batchSize = 1000;
// 2. 分批处理,每批之间释放连接
for (int i = 0; i < totalCount; i += batchSize) {
try {
processBatch(i, batchSize);
} catch (Exception e) {
log.error("批次处理失败", e);
// 失败后继续下一批,而不是一直占用连接
}
// 每批完成后短暂休眠,让其他线程有机会获取连接
Thread.sleep(100);
}
}
案例4:MyBatis 连接池管理问题
一体化问题处理(连接占完)
一体化系统性能分析(推测是mybatis连接池管理问题)
现象:连接池满了,但代码看起来没问题(都用了 try-with-resources)。
根因:项目用的 MyBatis 自带连接池(POOLED),poolMaximumActiveConnections 只配了 10个,高峰期远远不够。同时嵌套事务的传播行为配置不当,导致连接占用时间过长。
排查过程——先判断是池太小还是泄漏:
| 原因 | 特征 | 排查方式 |
|---|---|---|
| 连接池太小 | 高峰期定时耗尽,重启后正常,负载下来后也正常 | 调大连接池观察 |
| 连接泄漏 | 越来越严重,即使低峰也耗尽 | 检查代码是否忘关连接 |
临时把 poolMaximumActiveConnections 从 10 调到 30 观察一周,依然耗尽——确认是连接泄漏。
代码泄漏点定位:
// 嫌疑一:手动获取Connection后没有在finally中关闭
Connection conn = session.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery();
// rs、ps、conn 都没有关闭!
// 嫌疑二:事务未正常提交/回滚
DBUtil.BeginTrans(false);
// 如果中间抛异常,EndTrans没调用,连接不归还
// 嫌疑三:大数据导出ResultSet流式读取未关闭
ResultSet result = 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 连接池配置
MongoClientOptions options = MongoClientOptions.builder()
.connectionsPerHost(100) // 每个主机的最大连接数
.minConnectionsPerHost(10) // 最小连接数
.threadsAllowedToBlockForConnectionMultiplier(5)
.connectTimeout(5000) // 连接超时
.socketTimeout(30000) // Socket超时
.maxWaitTime(10000) // 最大等待时间
.build();
MongoClient client = new MongoClient(
Arrays.asList(
new ServerAddress("192.168.1.1", 27017),
new ServerAddress("192.168.1.2", 27017),
new ServerAddress("192.168.1.3", 27017)
),
options
);
连接池配置大全
Tomcat JDBC Pool
<Resource name="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
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="jdbc:oracle:thin:@localhost:1521:orcl" />
<property name="username" value="user" />
<property name="password" value="pass" />
<property name="initialSize" value="10" />
<property name="minIdle" value="10" />
<property name="maxActive" value="100" />
<property name="maxWait" value="30000" />
<property name="validationQuery" value="SELECT 1 FROM DUAL" />
<property name="testOnBorrow" value="true" />
<property name="testWhileIdle" value="true" />
<property name="timeBetweenEvictionRunsMillis" value="60000" />
<property name="removeAbandoned" value="true" />
<property name="removeAbandonedTimeout" value="60" />
<property name="logAbandoned" value="true" />
<!-- 慢SQL监控 -->
<property name="filters" value="stat,wall,log4j" />
</bean>
HikariCP(推荐)
spring:
datasource:
hikari:
maximum-pool-size: 50
minimum-idle: 10
idle-timeout: 300000
max-lifetime: 600000
connection-timeout: 30000
connection-test-query: SELECT 1 FROM DUAL
leak-detection-threshold: 60000
诊断工具
1. 查看数据库连接数
-- Oracle 查看当前连接
SELECT username, count(*) FROM v$session
WHERE username IS NOT NULL
GROUP BY username;
-- 查看具体连接信息
SELECT sid, serial#, username, machine, program, status
FROM v$session
WHERE username = 'APP_USER'
ORDER BY status;
2. 查看应用连接池(JMX)
# 通过 JMX 查看 Tomcat 连接池
jconsole localhost:1099
# → MBeans → Catalina → DataSource → connectionPool
3. 查看线程堆栈
# 找出哪些线程在等待连接
jstack <pid> | grep -A 20 "POOL EXHAUSTED"
jstack <pid> | grep -A 20 "borrowConnection"
4. 数据库级排查
-- Oracle 查看锁
SELECT
object_name,
session_id,
oracle_username,
os_user_name,
locked_mode
FROM v$locked_object lo, dba_objects do
WHERE lo.object_id = do.object_id;
-- 查看长时间运行的SQL
SELECT
sid, serial#, username,
ROUND(elapsed_seconds/60, 2) as minutes,
sql_text
FROM v$session s, v$sql q
WHERE s.sql_id = q.sql_id
AND s.status = 'ACTIVE'
AND s.username IS NOT NULL
ORDER BY elapsed_seconds DESC;
常见原因总结
| 原因 | 表现 | 解决方案 |
|---|---|---|
| 代码未归还连接 | 活跃连接数持续上升 | 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% 就告警
更多推荐
所有评论(0)