Spring Batch 是 Spring 生态里专门解决批处理(Batch Processing)的轻量级框架:它把“读取—处理—写入”抽象成可重试、可跳错、可断点续跑的 Job 与 Step。当业务出现“每晚同步百万条订单”“定时清洗日志”“大表批量导出”这类需求时,手写 for 循环既难维护又容易在半夜崩。本文用可运行示例,带你从零落地一个生产级批处理任务,并讲清调度、分片与故障恢复。
一、为什么需要批处理框架
批处理与在线请求(OLTP)是两类完全不同的负载:在线接口追求低延迟、单次少量;批处理追求高吞吐、可重入、可观测。一个典型的对账任务要扫描百万行、做聚合、写结果表,中间还可能遇到脏数据、网络抖动、数据库超时。如果只用 @Scheduled 套一层循环,一旦中途失败就只能从头再来,且无法知道“跑到第几条”。
Spring Batch 的价值恰恰在这里:它用 Spring Boot 3 的自动配置把 JobRepository(作业仓库)、事务管理、重试跳过等能力开箱即用,让你专注于“业务逻辑”而非“容错脚手架”。下面是一个最小可运行的批处理 Job。
二、核心概念:Job、Step 与 Chunk
2.1 引入依赖
// pom.xml(Spring Boot 3 + Spring Batch 5)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
Spring Batch 需要一个 JobRepository 来记录每次运行的元数据(哪一步成功、哪一条失败)。开发期用内存或 embedded 库即可,生产环境建议指向独立的 PostgreSQL 实例,避免与业务库互相干扰。
2.2 第一个 Chunk 模式的 Job
@Configuration
public class UserSyncJobConfig {
@Bean
public Job userSyncJob(JobRepository repo, Step syncStep) {
return new JobBuilder("userSyncJob", repo)
.start(syncStep)
.build();
}
@Bean
public Step syncStep(JobRepository repo,
PlatformTransactionManager tx,
ItemReader<User> reader,
ItemProcessor<User, User> processor,
ItemWriter<User> writer) {
return new StepBuilder("syncStep", repo)
.<User, User>chunk(500, tx) // 每 500 条提交一次事务
.reader(reader)
.processor(processor)
.writer(writer)
.build();
}
}
chunk(500) 是 Spring Batch 的精髓:它按“读 500 条→处理→批量写→提交事务”的节奏推进,内存里永远只驻留一个 chunk 的数据,因此处理上亿行也不会 OOM。这与 Java 21 虚拟线程 的“海量轻量并发”思路互补——Batch 用批量边界控内存,虚拟线程用轻量载体提吞吐。
三、读-处理-写:ItemReader / Processor / Writer
// 从数据库分页读取
@Bean
public JdbcPagingItemReader<User> reader(DataSource ds) {
JdbcPagingItemReader<User> r = new JdbcPagingItemReader<>();
r.setDataSource(ds);
r.setPageSize(500);
r.setRowMapper((rs, i) -> new User(rs.getLong("id"), rs.getString("name")));
r.setQueryProvider(new PostgresPagingQueryProvider() {{
setSelectClause("select id, name from users");
setSortKeys(Map.of("id", Order.ASCENDING));
}});
return r;
}
// 处理:脱敏 + 补全
@Bean
public ItemProcessor<User, User> processor() {
return u -> {
u.setName(mask(u.getName()));
return u;
};
}
// 写出:批量入库
@Bean
public JdbcBatchItemWriter<User> writer(DataSource ds) {
JdbcBatchItemWriter<User> w = new JdbcBatchItemWriter<>();
w.setDataSource(ds);
w.setSql("insert into users_clean(id, name) values (:id, :name)");
w.setItemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<>());
return w;
}
三元组职责清晰:Reader 只管“从哪来”,Processor 做转换与校验,Writer 负责“写到哪”。三者可独立单测,也方便复用——比如同一个 Processor 既支持同步也支持 ShardingSphere 分库分表 场景。
四、错误容忍:重试、跳过与回滚
生产数据的“脏”是常态。Spring Batch 提供了三层容错:retry(瞬时失败重试,如数据库死锁)、skip(跳过无法处理的坏行)、rollback(整个 chunk 回滚重来)。
| 策略 | 适用场景 | 配置要点 |
| retry | 瞬时异常:超时、死锁、连接闪断 | 限制次数 + 退避间隔,避免雪崩 |
| skip | 可预期的脏数据:格式错、空值 | 设 skip-limit,超限则整 Job 失败 |
| no-rollback | 业务校验失败,不希望回滚已写数据 | 声明哪些异常不触发回滚 |
return new StepBuilder("syncStep", repo)
.<User, User>chunk(500, tx)
.reader(reader).processor(processor).writer(writer)
.faultTolerant()
.retry(DeadlockLoserDataAccessException.class).retryLimit(3)
.backOffPolicy(new FixedBackOff(2000, 3)) // 2 秒退避,最多 3 次
.skip(ValidationException.class).skipLimit(100)
.build();
五、并行与分片:PartitionStep 加速
单线程 chunk 再快也有瓶颈。当数据可按某个键(如 user_id 区间、地区)切分,就用分区(Partition)把一份大 Step 拆成多个子 Step 并行跑,每个子 Step 处理一个区间。
@Bean
public Step partitionStep(JobRepository repo, Step workerStep) {
return new StepBuilder("partitionStep", repo)
.partitioner("workerStep", partitioner()) // 按 id 区间切 4 片
.step(workerStep)
.gridSize(4)
.taskExecutor(new SimpleAsyncTaskExecutor()) // 并行执行
.build();
}
@Bean
public Partitioner partitioner() {
return gridSize -> {
Map<String, ExecutionContext> map = new HashMap<>();
for (int i = 0; i < gridSize; i++) {
ExecutionContext ctx = new ExecutionContext();
ctx.putLong("minId", i * 100000L);
ctx.putLong("maxId", (i + 1) * 100000L);
map.put("partition" + i, ctx);
}
return map;
};
}
分区后每个 worker 拿到独立的 minId/maxId,Reader 加一句 where id between :minId and :maxId 即可互不干扰。若单机仍不够,可结合 Docker 把 worker 部署成多个容器,或用远程分区调度到不同节点。
六、断点续跑:JobRepository 与 restart
这是 Batch 最被低估的能力。由于每一步的进度都落在 JobRepository,任务中途崩溃后,重新启动同一 Job 会从失败点继续,而非从头。前提是 Job 名 + 参数(JobParameters)不变,框架据此识别“这是上次的续跑”。
# 首次运行
java -jar batch.jar --spring.batch.job.names=userSyncJob
# 崩溃后用相同参数重启,自动跳过已完成 chunk
java -jar batch.jar --spring.batch.job.names=userSyncJob
# 强制从头(清掉历史状态)
java -jar batch.jar --spring.batch.job.names=userSyncJob \
--spring.batch.job.enabled=false
配合 GitHub Actions 的定时工作流,可以把“跑批 + 失败自动重试”固化成 CI 流水线;若批处理涉及跨库同步,建议参考 MySQL 主从复制与读写分离 把读库与写库分离,避免长事务拖垮线上查询。
七、定时调度:从 @Scheduled 到外部触发
@SpringBootApplication
@EnableBatchProcessing
@EnableScheduling
public class BatchApp {
public static void main(String[] a) { SpringApplication.run(BatchApp.class, a); }
@Autowired JobLauncher jobLauncher;
@Autowired Job userSyncJob;
@Scheduled(cron = "0 30 2 * * ?") // 每天 02:30 跑批
public void runNightly() throws Exception {
JobParameters p = new JobParametersBuilder()
.addLong("time", System.currentTimeMillis()) // 每次参数不同=新实例
.toJobParameters();
jobLauncher.run(userSyncJob, p);
}
}
注意:若希望“每次都是独立运行实例”,JobParameters 必须带唯一值(如上例时间戳);若希望“崩溃可续跑”,则参数要保持稳定。两者语义相反,务必按业务选择。对于超大规模同步,也可完全脱离 @Scheduled,由 Kafka 消息积压治理 式的外部调度器触发,获得更好的可观测性。
八、监听器与失败告警:让批处理“会说话”
批处理大多跑在半夜,等天亮才发现失败就晚了。Spring Batch 提供 JobExecutionListener / StepExecutionListener,让你在任务开始前、结束后、失败时钩入自定义逻辑,例如发送钉钉或飞书告警、写监控事件、更新健康度指标。把“失败”从“等你发现”变成“主动通知”,是生产化的重要一步。
@Bean
public Job userSyncJob(JobRepository repo, Step syncStep) {
return new JobBuilder("userSyncJob", repo)
.start(syncStep)
.listener(new JobExecutionListener() {
@Override public void afterJob(JobExecution je) {
long read = je.getStepExecutions().stream()
.mapToLong(StepExecution::getReadCount).sum();
if (je.getStatus() == BatchStatus.FAILED) {
alarm.send("userSyncJob 失败,已读 " + read + " 条");
} else {
alarm.send("userSyncJob 成功,共处理 " + read + " 条");
}
}
})
.build();
}
把告警接到 Redis 缓存设计 里维护的“跑批健康度”键,前端大盘就能实时显示每个 Job 的通过率与耗时。当 skip 数突然飙升,往往意味着上游数据格式变了——这正是批处理系统最该被提前发现的信号。监听器还能在失败后自动触发补偿 Step,比如把失败区间重新入队,交给 Kafka 消息队列入门 异步重试,进一步把“人工救火”变成“系统自愈”。
九、生产落地检查清单
| 项 | 推荐做法 |
| JobRepository | 独立库(如 PostgreSQL),不与业务库混用 |
| chunk 大小 | 500–1000,按行宽与内存压测调优 |
| 容错 | retry 限 3 次 + backoff;skip 设上限防静默丢数 |
| 幂等 | Writer 用 upsert / 唯一约束,支持重复跑 |
| 监控 | 暴露 Batch 指标,失败告警接 Prometheus |
| 大数据量 | 优先 Partition 分片,必要时多容器并行 |
Spring Batch 不是“又一个定时任务工具”,而是把批处理的工程复杂度(容错、续跑、可观测)沉淀成框架。当你面对百万级数据搬家、定时清洗、报表生成时,它比裸循环省下的不只是代码,更是半夜被叫醒的次数。把上面八条清单逐条落地,你就能拥有一个稳到可以安心睡觉的批处理系统。




