一个定时任务从 14 分钟优化到 2 分钟
一个定时任务从 14 分钟优化到 2 分钟
“我会 Spring Boot”很普通,”我解决过一个线上任务内存 15G+、耗时十几分钟的问题”才有价值。这篇文章复盘一次真实的性能优化。
问题现象
一个每天凌晨跑的数据同步任务,负责把外部系统的数据拉取、清洗、写入本地库。最初数据量小没感觉,随着数据积累到几百万条,问题集中爆发:
- 执行时间从 2 分钟一路涨到 14 分钟
- 内存峰值 15G+,频繁触发 Full GC
- 偶尔直接把服务 OOM 打挂
排查过程
第一步:定位耗时点
在任务里埋了几个计时点,发现 90% 的时间都花在”写入数据库”这一步,而不是拉取或清洗。
第二步:找到内存杀手
用 JVM 的堆 dump 分析,发现内存里堆了 几十万个待写入的对象。原因是一开始的写法是”查一条、改一条、写一条”,后来为了”提速”改成了”全部加载进内存、最后一次性批量写”——结果把内存打爆了。
第三步:分析根因
真正的根因是没有分批(batching)+ 逐条 UPDATE 没有走批量。数据量大之后,逐条执行的开销是指数级增长的。
优化方案
1. 流式处理 + 分批
不再把所有数据 load 进内存,改用游标流式读取,每 1000 条处理一批:
1 | // 伪代码:分批处理,避免一次性加载 |
2. 批量写入
把逐条 INSERT/UPDATE 改成 JDBC 批处理,减少网络往返和事务开销。
3. 控制内存水位
每一批处理完就释放引用,让对象能及时被回收,内存峰值从 15G 降到 800M 左右。
结果
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 执行时间 | 14 分钟 | 2 分钟 |
| 内存峰值 | 15G+ | ~800M |
| Full GC | 频繁 | 基本无 |
小结
这次优化的教训很朴素:性能问题大多是”量变引起质变”,而根因往往是一个不起眼的写法。逐条处理和一次性全加载是两端,正确姿势永远在中间——分批 + 批量。做大数据量任务时,脑子里要始终绷着一根弦:内存水位和数据库往返次数。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 高晓庄 札记!
