一个定时任务从 14 分钟优化到 2 分钟

“我会 Spring Boot”很普通,”我解决过一个线上任务内存 15G+、耗时十几分钟的问题”才有价值。这篇文章复盘一次真实的性能优化。

问题现象

一个每天凌晨跑的数据同步任务,负责把外部系统的数据拉取、清洗、写入本地库。最初数据量小没感觉,随着数据积累到几百万条,问题集中爆发:

  • 执行时间从 2 分钟一路涨到 14 分钟
  • 内存峰值 15G+,频繁触发 Full GC
  • 偶尔直接把服务 OOM 打挂

排查过程

第一步:定位耗时点

在任务里埋了几个计时点,发现 90% 的时间都花在”写入数据库”这一步,而不是拉取或清洗。

第二步:找到内存杀手

用 JVM 的堆 dump 分析,发现内存里堆了 几十万个待写入的对象。原因是一开始的写法是”查一条、改一条、写一条”,后来为了”提速”改成了”全部加载进内存、最后一次性批量写”——结果把内存打爆了。

第三步:分析根因

真正的根因是没有分批(batching)+ 逐条 UPDATE 没有走批量。数据量大之后,逐条执行的开销是指数级增长的。

优化方案

1. 流式处理 + 分批

不再把所有数据 load 进内存,改用游标流式读取,每 1000 条处理一批:

1
2
3
4
5
6
7
8
9
10
// 伪代码:分批处理,避免一次性加载
int batchSize = 1000;
List<Record> batch = new ArrayList<>(batchSize);
while (cursor.hasNext()) {
batch.add(cursor.next());
if (batch.size() >= batchSize) {
processor.process(batch); // 清洗 + 写库
batch.clear();
}
}

2. 批量写入

把逐条 INSERT/UPDATE 改成 JDBC 批处理,减少网络往返和事务开销。

3. 控制内存水位

每一批处理完就释放引用,让对象能及时被回收,内存峰值从 15G 降到 800M 左右。

结果

指标 优化前 优化后
执行时间 14 分钟 2 分钟
内存峰值 15G+ ~800M
Full GC 频繁 基本无

小结

这次优化的教训很朴素:性能问题大多是”量变引起质变”,而根因往往是一个不起眼的写法。逐条处理和一次性全加载是两端,正确姿势永远在中间——分批 + 批量。做大数据量任务时,脑子里要始终绷着一根弦:内存水位和数据库往返次数。