Spring Batch 实战:批处理任务调度、分片与断点续跑

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 不是“又一个定时任务工具”,而是把批处理的工程复杂度(容错、续跑、可观测)沉淀成框架。当你面对百万级数据搬家、定时清洗、报表生成时,它比裸循环省下的不只是代码,更是半夜被叫醒的次数。把上面八条清单逐条落地,你就能拥有一个稳到可以安心睡觉的批处理系统。

上一篇 Envoy 实战:用高性能代理与 API 网关治理微服务流量
下一篇 Apache Paimon 实战:流式湖仓一体与实时数仓落地
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]