线程池占满与数据库优化实战:从连接池耗尽到性能调优

本文最后更新于 2026-08-04 15:16

线程池占满与数据库优化实战:从连接池耗尽到性能调优

本文记录了一次生产环境的真实故障排查过程:批量替数任务执行时,数据库连接池耗尽导致服务不可用。从问题定位、根因分析到优化方案,完整呈现排查思路和解决策略。

一、问题背景

在执行替数任务时,两个调度任务同时报错:

  • 任务一orderReplace — 数据库连接池耗尽
  • 任务二asyncInsExecRepData — 数据库连接获取超时

两个任务的核心报错信息相同:

1
GetConnectionTimeoutException: wait millis 30000, active 50, maxActive 50, creating 0

关键信息解读

字段 含义
wait millis 30000 等待获取连接的时间(30秒)
active 50 当前活跃连接数
maxActive 50 最大连接数
creating 0 正在创建的连接数

结论:50 个连接全部被占用,新请求等待 30 秒后超时。


二、问题定位

2.1 活跃连接都在做什么?

从报错堆栈的 runningSqlCount 可以看到连接池中 SQL 的分布:

SQL 类型 数量 说明
INSERT INTO SEND_SER_SET_DRAFT 1 插入草稿表
INSERT INTO SEND_SER_SET_HIS 47 插入历史表(占 94%)
SELECT SEQ_xxx.nextval 1 获取序列值
DELETE SEND_SER_SET_DRAFT 1 删除草稿表

核心问题:47 个连接都在执行同一张历史表的 INSERT 操作,占用了 94% 的连接池资源。

2.2 为什么会有这么多并发 INSERT?

替数任务的核心逻辑是:对每条记录执行”删除草稿 → 插入草稿 → 插入历史”的操作。当批量任务并发执行时,大量线程同时向历史表写入数据,导致连接池被耗尽。

2.3 第二个任务的报错

1
GetConnectionTimeoutException: wait millis 10000, active 50, maxActive 50

同样的连接池耗尽问题,等待 10 秒后超时。由于两个任务共享同一个数据库连接池,任务一耗尽了连接,任务二自然也无法获取连接。


三、根因分析

3.1 直接原因

批量替数任务并发度太高,大量线程同时执行 INSERT 操作,耗尽了数据库连接池。

3.2 深层原因

  1. 批量任务并发控制不足:没有限制同时操作数据库的线程数
  2. 历史表写入量大:每条记录都需要写入历史表,数据量大时 INSERT 语句多
  3. 连接池配置偏小maxActive=50 在高并发批量场景下不够用
  4. 缺少流量控制:没有对批量操作进行限流或分批处理

3.3 调用链路分析

从堆栈信息可以还原调用链路:

1
2
3
4
5
6
HTTP 请求入口
→ Controller.batchRepl()
→ Service.updateBatchRepl()(Spring 事务开始)
→ 分布式锁(DLockAspect)
→ 批量操作数据库
INSERT INTO 历史表(大量并发)

注意:整个批量操作在一个大事务中,事务持有连接的时间很长。


四、优化方案

4.1 连接池参数调优

1
2
3
4
5
6
7
8
9
spring:
datasource:
druid:
maxActive: 100 # 适当增大最大连接数
minIdle: 20 # 最小空闲连接数
maxWait: 60000 # 最大等待时间(毫秒)
timeBetweenEvictionRunsMillis: 60000 # 检测间隔
minEvictableIdleTimeMillis: 300000 # 最小空闲时间
validationQuery: SELECT 1 FROM DUAL # 连接检测 SQL

调优原则

  • maxActive 不是越大越好,需要根据数据库服务器配置和业务场景调整
  • maxWait 适当增大,避免短暂高峰时立即超时
  • 配合连接有效性检测,避免使用已断开的连接

4.2 批量任务分批处理

将大批量任务拆分为小批次执行,控制并发度:

1
2
3
4
5
6
7
8
// 分批处理,每批 100 条
int batchSize = 100;
List<List<Long>> partitions = Lists.partition(allIds, batchSize);

for (List<Long> batch : partitions) {
// 每批独立事务,避免长事务
processBatch(batch);
}

关键点

  • 每批独立事务,避免一个超长事务长时间占用连接
  • 批次之间可以适当休眠,给连接池恢复时间
  • 批次大小需要根据实际数据量和连接池大小调整

4.3 事务优化

问题:原实现中整个批量操作在一个大事务中,事务持有连接的时间过长。

优化:将大事务拆分为小事务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 优化前:一个大事务
@Transactional
public void updateBatchRepl(List<Long> ids) {
for (Long id : ids) {
// 删除草稿、插入草稿、插入历史
// 全部在一个事务中
}
}

// 优化后:每条记录独立事务
public void updateBatchRepl(List<Long> ids) {
for (Long id : ids) {
processSingle(id); // 每条记录独立事务
}
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void processSingle(Long id) {
// 删除草稿、插入草稿、插入历史
}

4.4 并发控制

使用信号量或线程池控制并发度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 方案一:信号量控制数据库并发
private final Semaphore dbSemaphore = new Semaphore(20); // 最多20个并发

public void processWithLimit(Long id) {
dbSemaphore.acquire();
try {
processSingle(id);
} finally {
dbSemaphore.release();
}
}

// 方案二:使用固定大小的线程池
ExecutorService executor = new ThreadPoolExecutor(
10, // 核心线程数
20, // 最大线程数
60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadPoolExecutor.CallerRunsPolicy() // 队列满时由调用线程执行
);

4.5 SQL 优化

批量 INSERT 优化

1
2
3
4
5
6
-- 优化前:逐条插入
INSERT INTO table (col1, col2) VALUES (val1, val2);
INSERT INTO table (col1, col2) VALUES (val3, val4);

-- 优化后:批量插入
INSERT INTO table (col1, col2) VALUES (val1, val2), (val3, val4);

序列获取优化

1
2
3
4
5
-- 优化前:每条记录获取一次序列
SELECT SEQ_xxx.nextval FROM dual; -- 执行 N 次

-- 优化后:一次获取多个序列值(Oracle)
SELECT SEQ_xxx.nextval FROM dual CONNECT BY LEVEL <= 100;

五、监控与预警

5.1 连接池监控

Druid 自带监控功能,开启后可以实时查看连接池状态:

1
2
3
4
5
6
7
8
9
spring:
datasource:
druid:
stat-view-servlet:
enabled: true
url-pattern: /druid/*
web-stat-filter:
enabled: true
url-pattern: /*

5.2 关键指标预警

指标 预警阈值 说明
活跃连接数 / 最大连接数 > 80% 连接池即将耗尽
等待获取连接的线程数 > 5 有请求在排队
连接获取平均耗时 > 500ms 连接池压力过大
长事务数量 > 3 可能存在未释放的连接

5.3 日志增强

在关键位置添加日志,便于排查:

1
2
3
4
5
// 连接池状态日志
log.info("Druid pool stats - active: {}, pooling: {}, waitThreadCount: {}",
dataSource.getActiveCount(),
dataSource.getPoolingCount(),
dataSource.getWaitThreadCount());

六、优化效果

指标 优化前 优化后
最大活跃连接数 50(经常打满) 50(峰值约 30)
连接获取超时 频繁发生 未再发生
批量任务执行时间 超时失败 正常完成
事务持有连接时间 数分钟 秒级

七、总结

7.1 排查思路

1
2
3
4
报错信息 → 识别问题(连接池耗尽)
→ 分析 runningSqlCount(47INSERT占满连接池)
→ 还原调用链路(大事务 + 高并发)
→ 定位根因(批量操作缺少并发控制)

7.2 优化核心原则

  1. 大事务拆小事务:减少单个事务持有连接的时间
  2. 批量操作分批执行:控制并发度,避免瞬间打满连接池
  3. 连接池参数合理配置:根据业务场景调整,不是越大越好
  4. SQL 批量化:减少数据库交互次数
  5. 监控预警:及时发现连接池压力,防患于未然

7.3 避坑指南

  • ❌ 不要在批量操作中使用一个大事务
  • ❌ 不要让批量任务无限制并发
  • ❌ 不要忽视连接池的监控指标
  • ✅ 分批处理 + 独立事务 + 并发控制
  • ✅ 合理配置连接池参数
  • ✅ 建立完善的监控预警机制

线程池占满与数据库优化实战:从连接池耗尽到性能调优
https://your-project-name.pages.dev/2026/07/25/threadpool-db-optimization/
作者
阿川
发布于
2026年7月25日
许可协议