批处理“失联”又“重跑”Spring Batch 监控缺失与重启暴雷你的作业还安全吗凌晨两点ETL 作业跑了 6 个小时你不知道它卡在哪一步也不敢重启怕数据重复唯一知道的是数据库连接池快满了CPU 却在摸鱼。终于作业因为一条“脏数据”崩了你想从失败点重启却发现它根本不是从断点继续而是把所有数据又处理了一遍主键冲突炸穿日志。更绝望的是Spring Boot Actuator 只告诉你“作业正在运行”其他一概不知。这不是批处理的宿命而是你缺少一套透明的监控体系和精准的重启机制。本文深入 Spring Boot Spring Batch 的批处理监控与重启疑难杂症从元数据表解读、进度暴露、失败恢复策略到优雅重启给你一套让批处理从“盲盒”变“透明”的完整方案确保每一次重启都精准落在断裂的齿扣上。一、血泪现场监控失明与重启陷阱1.1 执行进度成谜排查全靠猜一个数据迁移作业跑了 5 小时日志一直在滚动但没人知道到底迁移了多少条还有多少没迁。运维只能在数据库里SELECT COUNT(*)对比但数据在变化结果不准。作业最终失败大量时间被浪费。1.2 失败后重启数据重复写入你点击重启Spring Batch 确实“从失败步骤继续”但你的ItemWriter没做幂等之前已成功写入的 10 万条数据被再次插入主键冲突导致大量异常或者重复记录污染业务表。1.3 只想跳过坏记录结果整个作业都被跳过了你配置了skip-limit10以为只跳过 10 条坏数据。但作业重启后框架重新处理了所有记录导致之前被跳过的错误又被触发加上新的错误跳过计数累计直接超过上限作业再次失败。1.4 没人知道作业失败了作业异常终止没有告警。直到第二天业务方询问报表为何没出你才发现数据库里的BATCH_JOB_EXECUTION状态是FAILED但无人知晓。这些痛苦源于两个缺失运行时的可观测性和失败后的精确恢复策略。Spring Batch 其实内置了强大的元数据持久化和重启能力但你需要正确配置和使用。二、Spring Batch 的监控根基四大元数据表Spring Batch 将作业和步骤的执行状态持久化到数据库中默认表前缀BATCH_这是监控和重启的基础。BATCH_JOB_INSTANCE作业实例由jobName和唯一jobKey识别参数决定。BATCH_JOB_EXECUTION每次作业执行记录包含START_TIME,END_TIME,STATUS,EXIT_CODE。BATCH_STEP_EXECUTION步骤执行记录包含READ_COUNT,WRITE_COUNT,FILTER_COUNT,COMMIT_COUNT,STATUS等。BATCH_JOB_EXECUTION_PARAMS执行参数。监控的起点就是查询这些表。Spring Batch 提供了JobExplorerAPI 来访问它们并可通过 Actuator 暴露端点。三、方案一利用 Spring Boot Actuator 快速暴露作业状态Spring Batch 集成自动配置了JobLauncherCommandLineRunner并且 Actuator 可以提供基本的作业信息。开启批处理端点Spring Boot 2.x 和 3.x 略有不同management:endpoints:web:exposure:include:health,info,metrics,scheduledtasks,batchendpoint:batch:enabled:true访问/actuator/batch会返回最近执行的作业概要。但信息有限还需要自定义增强。3.1 自定义 Actuator 端点暴露详细进度通过JobExplorer获取正在运行的步骤的读写计数计算进度。ComponentEndpoint(idbatch-progress)publicclassBatchProgressEndpoint{AutowiredprivateJobExplorerjobExplorer;ReadOperationpublicMapString,ObjectgetProgress(SelectorStringjobName){// 查找最近一个正在运行的作业实例JobInstancejobInstancejobExplorer.getLastJobInstance(jobName);if(jobInstancenull)returnMap.of(status,NOT_FOUND);ListJobExecutionexecutionsjobExplorer.getJobExecutions(jobInstance);JobExecutionlatestexecutions.get(executions.size()-1);// 收集步骤执行信息ListMapString,ObjectstepsnewArrayList();for(StepExecutionse:latest.getStepExecutions()){steps.add(Map.of(stepName,se.getStepName(),status,se.getStatus().toString(),readCount,se.getReadCount(),writeCount,se.getWriteCount(),commitCount,se.getCommitCount()));}returnMap.of(jobId,latest.getJobId(),status,latest.getStatus().toString(),startTime,latest.getStartTime(),steps,steps);}}现在通过/actuator/batch-progress/jobName就可以实时看到进度。3.2 Micrometer 指标推送 PrometheusSpring Batch 自动注册 Micrometer 指标spring_batch_job_duration_seconds,spring_batch_job_status,spring_batch_step_read_count等。只需添加micrometer-registry-prometheus配置 Actuator 暴露 Prometheus 端点即可在 Grafana 中可视化。自定义 Gauge 追踪进度百分比BeanpublicMeterBinderbatchProgressMetrics(JobExplorerjobExplorer){returnregistry-{Gauge.builder(batch.job.progress,()-{// 根据预估总量和已读数量计算百分比需要业务传入总量// 这里简化为读取计数returngetCurrentReadCount();}).register(registry);};}四、方案二JobExecutionListener 与 StepExecutionListener 记录详细日志与告警4.1 全局监听器记录生命周期ComponentpublicclassBatchJobNotificationListenerimplementsJobExecutionListener,StepExecutionListener{AutowiredprivateNotificationServicenotificationService;OverridepublicvoidbeforeJob(JobExecutionjobExecution){notificationService.send(Job jobExecution.getJobInstance().getJobName() started);}OverridepublicvoidafterJob(JobExecutionjobExecution){if(jobExecution.getStatus()BatchStatus.FAILED){StringmsgJob FAILED: jobExecution.getExitStatus().getExitDescription();notificationService.sendAlert(msg);}}OverridepublicExitStatusafterStep(StepExecutionstepExecution){log.info(Step {}: read{}, write{}, commit{}, status{},stepExecution.getStepName(),stepExecution.getReadCount(),stepExecution.getWriteCount(),stepExecution.getCommitCount(),stepExecution.getStatus());returnstepExecution.getExitStatus();}}4.2 长时间运行任务告警在beforeStep中记录时间戳在定时检查线程或通过StepExecution的getStartTime计算持续时间当超过阈值时告警。可与外部调度平台集成。五、方案三精准重启——让作业从断点继续而非重头再来Spring Batch 的“从失败处重启”是建立在执行上下文持久化和幂等读写之上的。5.1 利用 ExecutionContext 保存断点位置在ItemReader中将当前读取位置如分页偏移、文件行号保存在StepExecution的ExecutionContext中当步骤失败后下次重启时读取器会从这个位置继续。publicclassRetryableItemReaderimplementsItemReaderString{privateintcurrent0;privateinttotal1000;BeforeSteppublicvoidrestoreState(StepExecutionstepExecution){if(stepExecution.getExecutionContext().containsKey(current)){this.currentstepExecution.getExecutionContext().getInt(current);}}OverridepublicStringread(){if(currenttotal)returnnull;StringdatafetchData(current);// 在合适的时机更新上下文如每 chunk 提交时ExecutionContextctxStepSynchronizationManager.getContext().getStepExecution().getExecutionContext();ctx.putInt(current,current1);current;returndata;}}当步骤在某个 chunk 失败时Spring Batch 会回滚事务并且ExecutionContext中的current会保留在失败前的值因为事务回滚了未提交的上下文更新实际上ExecutionContext的更新是在 chunk 提交后持久化所以失败 chunk 的更新不会保存下次重启会从上一个成功提交的位置继续。这个机制天然保证了重启只处理未完成的部分。5.2 非幂等 Writer 的重启保护如果写操作不是幂等的如发送邮件、调用非幂等 API重启会导致重复操作。解决方式将写操作设计为事务性且幂等如 SQL INSERT IGNORE 或 Merge。对于外部服务在执行前检查StepExecution的writeCount和commitCount或者利用ItemWriter的beforeStep记录已处理条数但更可靠的是使用“已处理日志表”。使用JobParameters的唯一标识如run.id配合业务唯一键避免重复。5.3 跳过已处理记录与重试Spring Batch 提供了skip-limit和retry-limit。但重启时skip 计数会重置除非使用ExecutionContext记录。如果希望重启后继续保留跳过配额需要在SkipPolicy中自定义结合数据库记录已跳过的记录标识。更好的做法是对于可预见的脏数据用ItemProcessor返回 null 来过滤而非依赖 skip。5.4 重启失败的作业通过JobOperator可以命令行或 API 重启AutowiredprivateJobOperatorjobOperator;publicvoidrestartLastFailedJob(StringjobName){JobInstancejobInstancejobExplorer.getLastJobInstance(jobName);ListJobExecutionexecutionsjobExplorer.getJobExecutions(jobInstance);JobExecutionlastExecutionexecutions.get(executions.size()-1);if(lastExecution.getStatus()BatchStatus.FAILED){jobOperator.restart(lastExecution.getId());}}或者使用 Spring Batch Admin 等工具。注意事项重启相同的JobParameters会直接返回原来的JobInstance并继续未完成的步骤。如果参数有变化会创建新实例无法从旧实例重启。因此需要保持作业识别参数如日期不变而通过run.id等非识别参数区分每次执行。六、方案四健康检查与元数据表维护6.1 元数据表膨胀问题长时间运行后元数据表会积累大量历史数据影响查询性能。Spring Batch 提供JobRepository.deleteJobExecution()等方法可以编写定时任务清理旧记录。Scheduled(cron0 0 4 * * ?)publicvoidcleanUpHistory(){for(StringjobName:jobNames){ListJobInstanceinstancesjobExplorer.findJobInstancesByJobName(jobName,0,100);// 保留最近10个实例删除更旧的}}注意删除作业实例会级联删除执行记录和上下文。谨慎操作确保不再需要这些审计数据。6.2 监控元数据表本身查询BATCH_JOB_EXECUTION中的STATUSSTARTED长时间未更新的记录可能是僵尸作业进程被杀但状态未更新。检测并报警必要时手动标记为FAILED或ABANDONED。6.3 与外部调度平台集成Spring Batch 的作业可以由 Spring Cloud Data Flow、Kubernetes CronJob 或 XXL-JOB 触发这些平台可以提供额外的监控面板和重启接口但作业自身的元数据仍是最底层保障。七、常见疑难杂症速查表现象根因解决方案重启后从头开始而非从断点ItemReader 未持久化状态到 ExecutionContext实现ItemStream或使用BeforeStep/AfterStep管理状态重启导致数据重复Writer 非幂等或者状态保存与实际写入不一致设计幂等写或使用唯一键去重确保状态更新在事务内跳过记录在重启后再次导致失败skip count 重置跳过策略未持久化记录已跳过的记录 ID重启时通过 Reader 过滤作业执行状态为 UNKNOWN进程被杀元数据未更新启动时检测并标记FAILED配合JobExplorer修复Actuator 端点暴露的信息过少默认仅返回最后几次执行概要自定义 Endpoint或使用 Spring Batch Admin UI元数据表查询慢数据过多未清理定期清理历史添加索引并行作业的监控混乱多个 JobExecution 同时运行按JobInstance和JobExecution ID区分Prometheus 标签使用job_name和status重启时丢失作业参数未将识别参数与非识别参数分开使用JobParametersBuilder时非识别参数不参与实例标识八、最佳实践打造可观测、可恢复的批处理必须启用数据库元数据持久化spring.batch.jdbc.initialize-schemaalways或手动建表确保重启能力。每个 ItemReader 实现状态保存使用ItemStreamSupport或ExecutionContext让作业可恢复。幂等 Writer 是黄金法则如果不能就用“已处理记录表”进行去重。全局监听器统一告警失败、完成、超时都要通知到人。利用 Micrometer 暴露进度指标读计数、写计数、作业耗时让 Grafana 大屏可见。合理分割 JobParameters业务日期等识别参数保持不变run.id等非识别参数每次递增。定期归档元数据保留适当历史避免性能退化。编写集成测试模拟失败重启验证状态恢复和幂等性防止上线后翻车。使用JobOperator提供管理 API允许手动停止、重启而不是直接操作数据库。对长时间运行的任务设置超时在Step中配置throttle-limit或通过TaskExecutor超时控制。九、结语让批处理从“黑箱”变“白盒”Spring Batch 为批处理提供了强大的持久化和重启框架但监控和恢复策略需要你亲手设计。当你把进度暴露到监控大屏把断点保存到执行上下文把幂等写入每条记录批处理就不再是凌晨的噩梦。现在检查你的作业ItemReader 有没有保存状态元数据表还能查吗失败后重启会重复写入吗补上这些空缺你就能安心入睡因为你的批处理知道在哪里跌倒就在哪里爬起。