Job-Instance-Execution 三层生命周期
本章定位:彻底理解 Spring Batch 中最核心的三层生命周期模型——Job、JobInstance 和 JobExecution 的关系,这是掌握重启机制和状态管理的基础。
定义与作用
Spring Batch 将一次批处理任务的生命周期划分为三个层次:
| 概念 | 含义 | 类比 |
|---|---|---|
| Job | 作业的静态定义,由 Step 组成,是编写的蓝图 | 一份菜谱——定义了做什么、怎么做 |
| JobInstance | Job + JobParameters 确定的逻辑运行实例 | 按菜谱做一顿饭——同样的菜谱 + 相同的食材 = 同一顿饭 |
| JobExecution | JobInstance 的物理执行尝试 | 这顿饭的第一遍炒菜(可能糊了,需要重新炒) |
飞翔科技后端开发小崔有一次面试被问到这三者的区别时,用了一个生动的比喻:
"Job 是代码里写的那个处理逻辑;JobInstance 是'用今天的文件跑这个逻辑'这个实例;JobExecution 是这次跑的过程记录——如果中途数据库挂了,修复后重新跑,就是同一个 JobInstance 的第二个 JobExecution。"
核心原理
生命周期流转
关键规则
| 规则 | 说明 |
|---|---|
| Job + JobParameters = JobInstance 的唯一标识 | 同一 Job + 同一组 JobParameters → 同一个 JobInstance |
| 同一 JobInstance 可以有多于一个 JobExecution | 首次执行失败后,用相同参数重启会产生新的 JobExecution |
| 已完成的 JobInstance 不能再次启动 | 状态为 COMPLETED 的 JobInstance 再次启动会抛出 JobInstanceAlreadyCompleteException |
| 失败的 JobInstance 可以重启 | 状态为 FAILED 的 JobInstance 可以创建新的 JobExecution |
状态流转详细
完整示例
场景一:飞翔科技——学生数据导入的失败与重启
背景:小崔开发的学生数据导入 Job 在第一次运行时,由于数据库连接超时在 Step 2 失败。他用相同的 JobParameters 重启后,Spring Batch 从 Step 2 开始恢复,跳过了已完成的 Step 1。
第一次执行——失败:
Job: [studentImportJob] with params: {input.file=students_20260613.csv, run.id=1}
JobInstance Id: 1
JobExecution Id: 1
Step 1 [validateStep]: COMPLETED (1000 records validated)
Step 2 [processStep]: FAILED (Connection timeout at record 542)
Step 3 [reportStep]: NOT STARTED
对应元数据表:
BATCH_JOB_INSTANCE:
JOB_INSTANCE_ID=1, JOB_NAME='studentImportJob', JOB_KEY='abc123'
BATCH_JOB_EXECUTION:
JOB_EXECUTION_ID=1, JOB_INSTANCE_ID=1, STATUS='FAILED'
BATCH_STEP_EXECUTION:
STEP_EXECUTION_ID=1, JOB_EXECUTION_ID=1, STEP_NAME='validateStep', STATUS='COMPLETED'
STEP_EXECUTION_ID=2, JOB_EXECUTION_ID=1, STEP_NAME='processStep', STATUS='FAILED'
第二次执行——重启成功:
Job: [studentImportJob] with params: {input.file=students_20260613.csv, run.id=1}
JobInstance Id: 1 ← 同一个 JobInstance!
JobExecution Id: 2 ← 新的 JobExecution
Step 1 [validateStep]: SKIPPED (already COMPLETED in previous execution)
Step 2 [processStep]: STARTED from record 542 (recovered from ExecutionContext)
Step 2 [processStep]: COMPLETED (458 records processed)
Step 3 [reportStep]: COMPLETED
关键行为:
- 使用相同的 JobParameters → 同一个 JobInstance,产生新的 JobExecution
- Step 1 因为已 COMPLETED → 自动跳过
- Step 2 从 ExecutionContext 中恢复进度 → 从第 542 条继续
- 使用
RunIdIncrementer时run.id自动递增,保证每次运行产生不同 JobInstance
场景二:电商订单处理——防止重复执行
背景:产品经理孔蓝担心同一份订单文件被重复处理导致重复发货。运营杨英需要用 Spring Batch 的 JobInstance 唯一性机制防止这种情况。
@Bean
public Job orderProcessJob(JobRepository jobRepository,
Step orderStep) {
return new JobBuilder("orderProcessJob", jobRepository)
.incrementer(new RunIdIncrementer()) // 每次启动 run.id 自增
.start(orderStep)
.build();
}
用不同日期参数启动——不同的 JobInstance:
// 6 月 13 日的订单
JobParameters params1 = new JobParametersBuilder()
.addString("orderDate", "2026-06-13")
.addLong("run.id", 1L)
.toJobParameters();
jobLauncher.run(orderProcessJob, params1); // → JobInstance #1
// 6 月 14 日的订单——不同参数,不同 JobInstance
JobParameters params2 = new JobParametersBuilder()
.addString("orderDate", "2026-06-14")
.addLong("run.id", 2L)
.toJobParameters();
jobLauncher.run(orderProcessJob, params2); // → JobInstance #2(新实例)
// 重复执行 6 月 13 日——相同参数,冲突!
JobParameters params3 = new JobParametersBuilder()
.addString("orderDate", "2026-06-13")
.addLong("run.id", 3L) // run.id 不同!
.toJobParameters();
jobLauncher.run(orderProcessJob, params3); // → JobInstance #3(run.id 让参数不同了!)
运行结果:
| 调用 | date | run.id | 结果 | 原因 |
|---|---|---|---|---|
| 第 1 次 | 2026-06-13 | 1 | 新 JobInstance #1,正常执行 | 新参数组合 |
| 第 2 次 | 2026-06-14 | 2 | 新 JobInstance #2,正常执行 | 新参数组合 |
| 第 3 次 | 2026-06-13 | 3 | 新 JobInstance #3 | run.id 不同 → 不同参数 → 新实例 |
这就是 RunIdIncrementer 的作用——通过自动递增 run.id,让每次执行产生不同的 JobParameters,从而创建新的 JobInstance。如果没有它,用相同的业务参数第二次启动会失败。
易错场景与避坑
反例一:不理解 JobInstance 唯一性导致"无法重启"
小崔在某次测试中,一个 Job 跑完后想重新跑一次验证,直接用相同的命令行参数:
java -jar batch-app.jar --spring.batch.job.name=studentImportJob input.file=students.csv
结果报错:
JobInstanceAlreadyCompleteException:
A job instance already exists and is complete for parameters={input.file=students.csv}.
If you want to run this job again, change the parameters.
原因:相同的 Job + 相同的 JobParameters 只能对应一个已完成的 JobInstance。
解决方案:
// 方案一:使用 RunIdIncrementer(推荐)
@Bean
public Job job(JobRepository jobRepository, Step step) {
return new JobBuilder("studentImportJob", jobRepository)
.incrementer(new RunIdIncrementer()) // 每次 run.id 自动 +1
.start(step)
.build();
}
// 方案二:添加时间戳参数
JobParameters params = new JobParametersBuilder()
.addString("input.file", "students.csv")
.addLong("time", System.currentTimeMillis()) // 每次不同
.toJobParameters();
反例二:误以为 JobInstance = 一次 App 启动
新来的前端开发黄俪在学 Spring Batch 时以为"每次启动 Spring Boot 应用就是一次 JobInstance":
// ❌ 错误理解:每次启动应用 = 新 JobInstance
// 实际上同一个 App 启动后可以执行多个 Job 和 JobInstance
@SpringBootApplication
public class BatchApp {
public static void main(String[] args) {
SpringApplication.run(BatchApp.class, args);
// Job 自动执行一次 → 这只是 1 个 JobExecution
}
}
正确理解:JobInstance 由 Job + JobParameters 决定,与应用进程无关。同一个 App 进程中可以:
// 一个 App 进程内执行多个 JobInstance
jobLauncher.run(job1, params1); // → JobInstance #1
jobLauncher.run(job1, params2); // → JobInstance #2(参数不同)
jobLauncher.run(job2, params3); // → 另一个 Job 的 JobInstance
面试高频考点
Q1:Job、JobInstance、JobExecution 三者的关系?
Job 是静态定义(代码中的 Bean)。JobInstance 是 Job + JobParameters 确定的逻辑实例。JobExecution 是 JobInstance 的物理执行尝试。一个 JobInstance 可以有多个 JobExecution(失败重启),但一个已完成(COMPLETED)的 JobInstance 不能再次启动。
Q2:什么是 JobParameters?为什么它是 JobInstance 的唯一性标识?
JobParameters 是启动 Job 时传入的键值对参数集合(如日期、文件路径、run.id)。Spring Batch 根据 Job 名称 + JobParameters 的哈希值来判断是否同一个 JobInstance。这是实现"相同任务不重复执行"和"失败任务可重启"两个需求的基础。
Q3:如何在每天定时执行同一个 Job 而不冲突?
① 使用
RunIdIncrementer让run.id每次递增;② 在 JobParameters 中加入日期参数(如addString("date", LocalDate.now().toString()));③ 加入时间戳参数。三种方式都能保证每次产生新的 JobInstance。
上一章:三层架构下一章:Chunk 处理模型