Chunk 处理模型
本章定位:彻底理解 Spring Batch 最核心的处理范式——Chunk-Oriented Processing。掌握 Chunk 的读写流程、事务边界和大小选择策略。
定义与作用
Chunk(数据块) 是 Spring Batch 面向块处理模式中的数据聚合单元。框架将逐条读取和处理的数据项累积到达到配置的 chunk-size 后,一次性地传递给 ItemWriter 进行批量写入。
飞翔科技架构师白歌在代码评审中经常强调:
"Chunk 不是简单的'每 N 条写一次'。它的关键设计是:在同一个事务内,逐条 read()、逐条 process(),最后一次性 write()。每一层都有独立的事务语义和容错策略。"
简而言之:
Chunk 处理 =
在一个事务中:
for i in 1..chunkSize:
item = ItemReader.read() // 逐条读
item = ItemProcessor.process(item) // 逐条处理
添加到缓冲区
ItemWriter.write(缓冲区) // 批量写
提交事务
核心原理
Chunk 处理的数据流
为什么 Chunk 处理高效
| 对比维度 | 逐条写入(Chunk Size = 1) | Chunk 批量写入(Chunk Size = 50) |
|---|---|---|
| SQL 执行 | 每行一次 INSERT(50 次网络往返) | 一次 batch INSERT 50 行(1 次网络往返) |
| 事务提交 | 每行 commit(50 次磁盘 I/O) | 每 chunk 一次 commit(1 次磁盘 I/O) |
| 失败回滚 | 仅当前行丢失 | 整个 Chunk 回滚(最多 49 条已处理数据需要重做) |
| 内存占用 | 低(逐条释放) | 中(缓冲 chunk-size 条) |
Chunk Size 选择策略
经验法则:
| Chunk Size | 适用场景 | 权衡 |
|---|---|---|
| 1 | 需要逐条写入的强一致性场景 | 性能最差,仅测试用 |
| 10 ~ 50 | 通用场景,均衡性能与内存 | 推荐默认范围 |
| 100 ~ 500 | 高吞吐、数据处理简单 | 内存占用增加 |
| 500 ~ 1000 | 极大规模、内存充足 | 事务时间长,失败回滚代价大 |
完整示例
场景一:飞翔科技学生管理系统——不同 Chunk Size 的性能对比
背景:小崔在优化学生数据导入性能时,产品经理孔蓝要求他在 30 秒内完成 10000 条数据的导入。他用不同 Chunk Size 做了基准测试。
测试配置:
@Bean
public Step importStep(JobRepository jobRepository,
PlatformTransactionManager tx,
ItemReader<Student> reader,
ItemWriter<Student> writer) {
return new StepBuilder("importStep", jobRepository)
.<Student, Student>chunk(50, tx) // ← 测试 1 / 10 / 50 / 100 / 500
.reader(reader)
.writer(writer)
.build();
}
性能测试结果:
| Chunk Size | 总耗时 | 事务数 | 吞吐量 | CPU 占用 | 内存峰值 |
|---|---|---|---|---|---|
| 1 | 45.2s | 10000 | 221/s | 15% | 32MB |
| 10 | 12.8s | 1000 | 781/s | 28% | 38MB |
| 50 | 5.3s | 200 | 1887/s | 42% | 45MB |
| 100 | 3.8s | 100 | 2631/s | 55% | 58MB |
| 500 | 2.9s | 20 | 3448/s | 68% | 110MB |
小崔的结论:Chunk Size 50 是最优平衡点——吞吐量是 Chunk Size 1 的 8.5 倍,但内存仅增加 13MB。
操作前后对比:
操作前(Chunk Size = 1):
Step [importStep] completed: 10000 read, 10000 written, 10000 commits
Duration: 45.2s
操作后(Chunk Size = 50):
Step [importStep] completed: 10000 read, 10000 written, 200 commits
Duration: 5.3s (↓ 88%)
场景二:电商订单处理——Chunk 内部分数据失败的处理
背景:运营部杨英处理订单 CSV 时,发现第 51 行数据格式异常。Chunk Size = 50 时,这条坏数据在第 2 个 Chunk 中,框架的行为是什么?
@Bean
public Step orderStep(JobRepository jobRepository,
PlatformTransactionManager tx,
FlatFileItemReader<Order> reader,
ItemWriter<Order> writer) {
return new StepBuilder("orderStep", jobRepository)
.<Order, Order>chunk(50, tx)
.reader(reader)
.writer(writer)
.faultTolerant()
.skip(FlatFileParseException.class)
.skipLimit(5)
.build();
}
行为分析:
运行输出:
Chunk 1: read 50 items, wrote 50 items, COMMIT
Chunk 2: read error at line 51 → SKIP (FlatFileParseException)
read 49 more items (lines 52-101)
wrote 49 items, COMMIT
skipCount = 1
Final: readCount=100, writeCount=99, skipCount=1, Chunks=2
关键理解:当 Write 阶段发生异常(非 Read 阶段)时,框架会执行一个特殊流程——扫描整个 Chunk 逐条写出以定位具体哪条数据导致问题。这是一个代价较高的操作,因此应优先让 Read 和 Process 阶段的 Skip 策略拦截问题数据。
易错场景与避坑
反例一:Chunk Size 过大导致事务超时
小崔在测试环境调优时将 Chunk Size 设为 2000,但在生产环境遇到了事务超时:
// ❌ Chunk Size 2000 + 每条约 50ms = 事务持续 100 秒 → 超时
return stepBuilderFactory.get("step")
.<Order, Order>chunk(2000)
.reader(reader)
.processor(slowProcessor) // 每条 50ms(含外部 API 调用)
.writer(writer)
.build();
// 错误:TransactionTimedOutException: Transaction timed out after 30 seconds
原因:每个 Chunk 是一个事务。Chunk Size 2000 × 50ms = 100 秒,超过了数据库事务超时时间。
解决方案:
// ✅ 根据单条处理时间反推 Chunk Size
// 单条耗时 50ms,期望事务最长 5s → Chunk Size ≤ 100
return new StepBuilder("step", jobRepository)
.<Order, Order>chunk(100, tx)
.reader(reader)
.processor(slowProcessor)
.writer(writer)
.build();
反例二:误以为 Processor 返回 null = 终止 Step
前端开发黄俪在学 Spring Batch 时有个误解:
// ❌ 误解:return null 会导致 Step 终止
@Override
public StudentEntity process(Student item) {
if (item.getAge() < 18) {
return null; // 黄俪以为这会让 Step 停止
}
return transform(item);
}
实际行为:Processor 返回 null 只表示过滤当前记录——该条数据不会传递给 Writer,但 Reader 会继续读取下一条。Step 只在 Reader.read() 返回 null 时才结束。
ItemProcessor的null= "这条跳过",ItemReader的null= "数据读完了"。
面试高频考点
Q1:Chunk-Oriented Processing 的工作流程是什么?
在一个事务内:① ItemReader 逐条 read()(最多 chunk-size 次),② ItemProcessor 逐条 process(),③ 累积到 chunk-size 条后,一次性调用 ItemWriter.write(List) 批量写入,④ 提交事务。框架不关心每条的单个结果,只关心整个 Chunk 能否成功提交。
Q2:Chunk Size 如何选择?
核心公式:Chunk Size × 单条处理时间 < 预期事务最大时长。通用场景推荐 10~50,高吞吐场景 100~500。同时考虑内存占用(Chunk Size 越大,缓冲区占用越大)和失败回滚代价(Chunk 越大,失败后需要重做的数据越多)。
Q3:Process 阶段的过滤(return null)和 Skip 阶段的跳过有什么区别?
Process 返回 null 是正常过滤——开发者明确知道这条数据不需要处理,不会计入 skipCount。Skip 是异常容错——Reader/Processor/Writer 抛出指定异常时,框架自动跳过并计入 skipCount,达到 skipLimit 后 Step 失败。