重启与重试
本章定位:区分并掌握 Spring Batch 的两个核心容错能力——Job 重启(Restart)和 Step 重试(Retry),理解它们的触发条件、限制和协作方式。
定义与作用
| 概念 | 粒度 | 触发条件 | 行为 |
|---|---|---|---|
| Job 重启 | Job 级别 | 上次 JobExecution 状态为 FAILED | 从失败的 Step 重新启动 |
| Step 重试 | Item 级别 | 当前 Item 处理抛出可重试异常 | 重新执行该 Item 的 Processor/Writer |
飞翔科技架构师白歌的区别:
"重启是整条流水线停电后重新合闸——从上次断掉的工位继续。重试是一个零件加工失败后再试一次——机器还在转,只是这个零件返工。"
核心原理
重启流程
重试流程
关键区分
| 维度 | 重启 | 重试 |
|---|---|---|
| 触发方式 | 手动重新 jobLauncher.run() | 框架自动 |
| 异常要求 | JobExecution FAILED | 异常类型匹配 retry() |
| 状态依赖 | 依赖 ExecutionContext | 不依赖持久化状态 |
| 适用异常 | 数据库连接断开、内存溢出 | 瞬时故障(锁超时、网络抖动) |
| 不适用异常 | — | 逻辑错误(NPE、数据校验失败) |
完整示例
场景一:断点续传——文件处理中途崩溃后重启
@Bean
public Step restartableFileStep(JobRepository jobRepository,
PlatformTransactionManager tx) {
return new StepBuilder("restartableFileStep", jobRepository)
.<String, String>chunk(50, tx)
.reader(new ResumableFileReader("data.txt")) // 实现 ItemStream
.writer(items -> {
for (String item : items) {
if (item.contains("BOOM")) {
throw new RuntimeException("Simulated crash at: " + item);
}
System.out.println("Processed: " + item);
}
})
.build();
}
运行过程:
第 1 次执行:
Chunk 1: 50 条 → COMMIT → ExecutionContext 记录行号 50
Chunk 2: 50 条 → COMMIT → ExecutionContext 记录行号 100
Chunk 3: 读到含 "BOOM" 的行 → write() 抛异常 → ROLLBACK
JobExecution: FAILED
第 2 次执行(相同参数):
从 ExecutionContext 恢复: current.line = 100
Chunk 3: 从第 101 行开始读 50 条 → 再次遇到 "BOOM" → FAILED
修复 BOOM 后第 3 次执行:
从 ExecutionContext 恢复: current.line = 100
Chunk 3: 50 条正常 → COMMIT
... 后续全部正常 → Job COMPLETED
场景二:重试配置——数据库死锁自动恢复
@Bean
public Step retryCapableStep(JobRepository jobRepository,
PlatformTransactionManager tx,
ItemReader<Order> reader,
ItemWriter<Order> writer) {
return new StepBuilder("retryCapableStep", jobRepository)
.<Order, Order>chunk(100, tx)
.reader(reader)
.writer(writer)
.faultTolerant()
.retry(DeadlockLoserDataAccessException.class) // 数据库死锁 → 重试
.retryLimit(3) // 最多 3 次
.skip(DataIntegrityViolationException.class) // 约束冲突 → 跳过
.skipLimit(50)
.build();
}
操作前后对比:
| 场景 | 无容错 | 有 retry + skip |
|---|---|---|
| 瞬时死锁(第 50 条) | Step FAILED,147 条白读 | 最多重试 3 次后恢复 |
| 约束冲突(第 200 条) | Step FAILED,200 条白读 | 跳过该条,继续处理 |
| 连续死锁(5 条) | Step FAILED | 前 3 条重试 3 次,后 2 条超出 → FAILED |
易错场景与避坑
反例一:preventRestart() 阻止重启
// ❌ 设置禁止重启 → Job 失败后无法恢复
return new JobBuilder("job", jobRepository)
.preventRestart()
.start(step)
.build();
// 失败后用相同参数再启动 → JobRestartException
反例二:retry 不加 retryLimit
// ❌ 只声明 retry 异常类型但不设上限 → 无限重试
.faultTolerant()
.retry(TransientException.class)
// 忘记 .retryLimit() → 默认 0 → 等效于不重试
面试高频考点
Q1:重启和重试的协作——重试耗尽后如何进入重启?
重试在 Item 级别,重启在 Job 级别。如果一条 Item 重试 3 次都失败且不匹配 skip,Step 失败 → Job 失败。此时可以修复数据后重启 Job——重启时框架跳过已成功 COMMIT 的 Chunk,从失败的 Chunk 重新开始。
Q2:哪些异常适合重试,哪些不适合?
适合重试:瞬时故障(死锁、网络超时、连接池耗尽)。不适合重试:数据错误(NullPointerException、格式错误、校验失败)——重试也不会改变结果。对于数据错误应使用 skip 而非 retry。