
本文详解如何在 spring batch 中安全、高效、可重启地处理 3000 万级数据——通过基于日期列的逻辑分片(非物理分表),兼顾并行读取、顺序写入、故障隔离与精准断点续跑。
本文详解如何在 spring batch 中安全、高效、可重启地处理 3000 万级数据——通过基于日期列的逻辑分片(非物理分表),兼顾并行读取、顺序写入、故障隔离与精准断点续跑。
在大规模数据迁移或 ETL 场景中,面对单表 3000 万行记录(如 table_a 含 event_date DATE 列),直接全量读取不仅易触发 OOM、超时或事务膨胀,更丧失错误定位与恢复能力。您提出的“按日切分 + 并行读取 + 顺序写入 + 精确重启”思路方向正确,但实现细节需严格遵循 Spring Batch 的设计范式——它不支持跨分区共享 Writer 或强制全局顺序写入,强行绕过框架约束将导致状态不一致、重复写入或无限循环等严重问题。
✅ 正确架构:分区驱动(Partitioning)+ 每分区独立 Chunk 处理
Spring Batch 原生推荐且最健壮的方案是 Step Partitioning(步骤分区),其核心思想是:
- 将大任务逻辑拆分为多个相互独立、可并行执行、自带事务边界的子任务(Partition);
- 每个分区拥有专属的
ExecutionContext、独立的 Reader/Processor/Writer 生命周期; - 失败时仅重跑失败分区,无需人工干预或状态补偿;
- 天然支持重启、跳过、重试、监听器等企业级特性。
您当前代码的问题在于:
- ❌
chunk(1)设置极小(应设为100–1000,取决于单日数据量与内存); - ❌
ItemReader extends List<tableaentity>></tableaentity>设计错误——Spring Batch Reader 必须返回单条记录(TableAEntity),而非List;批量封装应在 Processor 或自定义JdbcCursorItemReader的 SQL 层完成; - ❌
PartitionHandler缺少线程池配置,易因默认单线程导致“伪并行”; - ❌
DatePartitioner未校验endDate包含性(!currentDate.equals(endDate)会漏掉最后一天),且未处理跨年/闰日等边界。
✅ 推荐实现(关键代码修正)
// 1. 修正 Partitioner:确保包含 endDate,生成合理分区键
public class DatePartitioner implements Partitioner {
private LocalDate startDate;
private LocalDate endDate;
@Override
public Map<string executioncontext> partition(int gridSize) {
Map<string executioncontext> partitions = new HashMap();
LocalDate current = startDate;
int index = 0;
while (!current.isAfter(endDate)) { // ✅ 使用 isAfter 替代 equals
ExecutionContext ctx = new ExecutionContext();
ctx.put("startDay", current.toString());
ctx.put("endDay", current.toString()); // 每日一个分区
partitions.put("partition_" + index++, ctx);
current = current.plusDays(1);
}
return partitions;
}
}
// 2. 配置高性能 JdbcCursorItemReader(按日查询)
@Bean
@StepScope
public JdbcCursorItemReader<tableaentity> reader(
@Value("#{stepExecutionContext['startDay']}") String startDay,
@Value("#{stepExecutionContext['endDay']}") String endDay) {
JdbcCursorItemReader<tableaentity> reader = new JdbcCursorItemReader();
reader.setDataSource(dataSource);
reader.setSql("SELECT * FROM table_a WHERE event_date BETWEEN ? AND ?");
reader.setRowMapper(new BeanPropertyRowMapper(TableAEntity.class));
reader.setPreparedStatementSetter((ps) -> {
ps.setDate(1, Date.valueOf(startDay));
ps.setDate(2, Date.valueOf(endDay));
});
reader.setFetchSize(1000); // ✅ 关键:提升 JDBC 批量拉取效率
return reader;
}
// 3. 配置 Chunk 大小(非动态!建议 500)
@Bean
@StepScope
public Step slaveStep() {
return stepBuilderFactory.get("slaveStep")
.<tableaentity tablebentity>chunk(500) // ✅ 固定 chunkSize,平衡内存与事务粒度
.reader(reader(null, null))
.processor(processor())
.writer(writer())
.build();
}
// 4. 分区 Handler:启用多线程(需显式配置线程池)
@Bean
public PartitionHandler partitionHandler() {
TaskExecutorPartitionHandler handler = new TaskExecutorPartitionHandler();
handler.setStep(slaveStep());
handler.setGridSize(30); // ✅ 按天数设置分区总数(如30天→30个分区)
// ✅ 必须配置线程池,否则默认单线程
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8);
executor.setMaxPoolSize(16);
executor.setQueueCapacity(100);
executor.afterPropertiesSet();
handler.setTaskExecutor(executor);
return handler;
}</tableaentity></tableaentity></tableaentity></string></string>
⚠️ 关键注意事项与最佳实践
-
Chunk Size 不是“动态”的:Spring Batch 要求
chunkSize在运行时固定。若单日数据量差异极大(如工作日 vs 周末),可按业务规律预设多档策略(如weekdayChunk=1000,weekendChunk=300),通过ExecutionContext传参动态选择。 -
Writer 必须是幂等的:因分区独立,同一日数据可能被重跑。务必在
INSERT INTO table_b ... ON CONFLICT DO UPDATE(PostgreSQL)或MERGE(SQL Server)中实现 UPSERT,避免主键冲突或重复插入。 -
避免在 Processor 中缓存全量日数据:Processor 应逐条处理,若需跨记录关联(如聚合),改用
CompositeItemProcessor+ItemStream持久化中间状态。 -
监控与可观测性:为每个分区添加
StepExecutionListener,记录stepExecutionContext.get("startDay")及处理行数,写入审计表便于追踪。 -
替代方案对比:
- ❌
Tasklet:适合控制流逻辑,不适用海量数据流处理(无内置分页、缓冲、重试); - ❌ 自定义
ItemWriter强制串行:破坏 Spring Batch 事务边界,极易引发死锁或数据错乱; - ✅
JdbcBatchItemWriter:已针对批量插入优化,配合setBatchSize(100)可显著提升写入吞吐。
- ❌
✅ 总结:为什么这是最优解?
| 维度 | 分区方案(推荐) | 其他方案(不推荐) |
|---|---|---|
| 重启能力 | ✅ 精确到分区(如 partition_15 失败,仅重跑该日) |
❌ 全量重跑或手动定位断点 |
| 并发安全 | ✅ 每个分区独立事务,无共享状态 | ❌ 自定义串行 Writer 易引发竞争 |
| 资源可控 | ✅ 内存占用 = 单日最大数据量 × chunkSize | ❌ 全量加载 → OOM 风险极高 |
| 运维友好 | ✅ 日志清晰、指标可监控、异常可追溯 | ❌ 无限循环、状态丢失、调试困难 |
最终,Spring Batch 的力量不在于“定制数据管道”,而在于将复杂分布式任务抽象为可组合、可验证、可运维的声明式步骤。坚持其 Chunk-Oriented Step 模型,您不仅能稳定处理 3000 万数据,更能构建出经得起生产考验的健壮批处理架构。










