Java学习随笔:批量任务如何留出失败恢复的余地(2026-10-23)
写 Java 批量任务时,我很容易先想到一个循环:查询数据,逐条处理,全部结束后打印成功。这样的实现适合说明流程,却还没有回答真实运行中的一个问题:如果处理到一半停了,下一次从哪里继续?今天整理的这个小主题,就是怎样给批量任务留出恢复的余地,让失败之后仍能说清已经完成什么、还剩下什么。
可以想象一个给历史订单补充统计字段的任务。待处理数据有几十万条,程序读出订单,计算结果,再写回存储。假设已经成功更新了一部分订单,此时进程退出。直接重新运行虽然容易,但可能重复做大量工作;如果处理过程中还会发送通知或调用外部服务,重复执行的后果就更复杂。因此,恢复能力需要在任务设计时考虑,不能等到失败后再靠观察日志猜测进度。
首先要定义一次任务处理的数据范围。比如按照固定的业务时间窗口,再结合稳定的记录标识确定边界。如果任务每次查询的都是此刻满足条件的所有数据,执行期间又不断有新记录进入,所谓“处理完”就可能缺少清晰含义。对于需要覆盖一个确定集合的任务,可以保存本次运行的筛选条件和边界;后来新增的数据由下一次任务处理。边界是否需要快照级别的一致性,则要根据业务要求决定,不能把保存一个最大标识当成完整的数据快照。
分批处理的价值也不只是减少一次读取的数据量。批次还是一个便于观察和恢复的单位。一次装入全部对象,会把内存、连接占用和失败影响集中到同一个很大的过程里。把任务拆成有上限的小批次,可以在批次之间记录进度,检查停止信号,并让资源及时归还。不过批次越小也不一定越好,读取和提交的固定成本会更明显。批次大小需要结合记录体积、处理耗时和下游承受能力调整,而不是照搬一个看起来顺眼的数字。
读取顺序必须稳定。用变化中的结果集反复按照页码跳过若干条记录,在处理会改变筛选条件的场景里,容易发生遗漏。例如前一批更新后不再符合待处理条件,剩余结果整体前移,再按原来的偏移量取下一页,就可能跳过一段。可以考虑按稳定且唯一的顺序逐步推进游标,但也要确认排序字段是否会变化,以及并列值如何处理。选择什么分页方式,最终取决于数据变化方式和任务需要的覆盖语义。
接下来是进度记录的时机。若先保存“这一批已完成”,再提交业务更新,中途失败就可能把未完成的数据跳过去;若先提交更新,再保存进度,失败时又可能重新处理刚完成的一批。两个写入能够处于同一事务内时,可以利用这个边界保证它们一起成功或一起回滚。如果进度和副作用跨越了不同系统,就要接受它们之间存在中间状态,并为重复处理、结果核对或补偿设计明确规则。一个进度数字并不能自动证明对应的业务动作都已完成。
失败也需要分清层次。某一条记录的数据不符合要求,可以记下记录标识和原因,按照约定继续处理其他记录;数据库持续不可用,则通常需要暂停整个任务,避免把每一条都刷成失败。对于单条失败,应保留可重新定位的最小信息,形成独立的待复查范围,不能只写一行错误日志就永久跳过。同时还要记录成功、失败和跳过的数量,让任务结束时的结果能够对得上。
并发会让恢复问题进一步复杂。如果多个工作线程各自处理不同批次,最靠后的批次完成,并不意味着之前所有批次都已完成。此时用一个简单的最大进度覆盖全局状态,可能掩盖较早位置的失败。可以按分片或批次保存独立状态,也可以只推进连续完成的边界。无论采取哪种方式,都需要明确谁负责领取任务、如何避免同时领取,以及进程退出后未完成的工作怎样重新进入可处理状态。
最后,任务还应允许有序停止。收到停止请求以后,可以停止领取新批次,给正在处理的部分一个明确的结束机会,再记录可恢复的状态;如果只能强制中断,也要知道哪些工作下次可能重做。学习时可以用一小份数据,在读取后、写入后和记录进度前分别模拟中断,再观察恢复是否遗漏或重复。对我来说,一个可靠的批量任务,不只是在顺利运行时能够处理完数据,也是在中断之后,仍然能够解释状态,并从一个可信的位置继续向前。

浙公网安备 33010602011771号