Spring Batch处理器重复键异常处理:逆序读文件及跳文件实现咨询
Spring Batch重复键异常处理方案实现
核心思路
当处理器触发唯一键异常时,先切换当前文件的读取方向为逆序;若再次触发同一文件的重复键异常,则直接跳过当前文件,启动下一个待处理文件的读取流程,同时保留重启状态的可恢复性。
实现步骤
1. 自定义可切换方向的文件读取器
继承Spring Batch的FlatFileItemReader,扩展读取方向控制和文件切换能力:
- 新增读取方向枚举(
FORWARD/REVERSE),默认正向读取 - 重写
open(ExecutionContext executionContext)方法,根据当前方向初始化读取流:- 正向:沿用原生
FlatFileItemReader的开头读取逻辑 - 逆向:通过
RandomAccessFile定位到文件末尾,实现逐行反向读取(需兼容不同操作系统的换行符)
- 正向:沿用原生
- 添加
switchReadDirection()方法切换读取方向,switchToNextFile()方法切换至下一个文件并重置读取方向
2. 重复键异常的捕获与处理
在StepExecutionListener或ItemProcessor中捕获数据库唯一键异常(如SQLIntegrityConstraintViolationException):
- 第一次捕获异常:调用读取器的
switchReadDirection()方法,重置当前Step的执行上下文,重新启动Step - 第二次捕获同一文件的异常:调用读取器的
switchToNextFile()方法,若存在下一个文件则重置重试计数并重启Step,否则标记流程失败
3. 执行上下文状态管理
在读取器中保存当前文件索引、读取方向、重试计数等状态到ExecutionContext,确保Job重启时能直接恢复到中断前的状态,避免重复执行无效逻辑。
编码建议
自定义可切换方向的读取器示例
public class DirectionalFlatFileItemReader<T> extends FlatFileItemReader<T> { private enum ReadDirection { FORWARD, REVERSE } private ReadDirection currentDirection = ReadDirection.FORWARD; private List<Resource> fileResources; private int currentFileIndex = 0; private RandomAccessFile reverseFileReader; @Override public void open(ExecutionContext executionContext) throws Exception { // 恢复重启前的状态 if (executionContext.containsKey("currentFileIndex")) { currentFileIndex = executionContext.getInt("currentFileIndex"); currentDirection = ReadDirection.valueOf(executionContext.getString("readDirection")); } super.setResource(fileResources.get(currentFileIndex)); if (currentDirection == ReadDirection.REVERSE) { // 初始化逆向读取流 reverseFileReader = new RandomAccessFile(super.getResource().getFile(), "r"); reverseFileReader.seek(reverseFileReader.length()); // 替换为自定义逆向行映射器 setLineMapper(new ReverseLineMapper<>(getLineMapper())); } else { super.open(executionContext); } // 保存当前状态到执行上下文 executionContext.putInt("currentFileIndex", currentFileIndex); executionContext.putString("readDirection", currentDirection.name()); } public void switchReadDirection() { this.currentDirection = currentDirection == ReadDirection.FORWARD ? ReadDirection.REVERSE : ReadDirection.FORWARD; } public boolean switchToNextFile() { if (currentFileIndex < fileResources.size() - 1) { currentFileIndex++; currentDirection = ReadDirection.FORWARD; closeReverseReader(); return true; } closeReverseReader(); return false; } private void closeReverseReader() { if (reverseFileReader != null) { try { reverseFileReader.close(); } catch (IOException e) { // 日志记录关闭异常 } } } // 省略逆向行读取的具体实现,需处理换行符和编码兼容 }
异常处理监听器示例
@Component public class DuplicateKeyStepListener implements StepExecutionListener { @Autowired private DirectionalFlatFileItemReader<?> fileReader; private int duplicateRetryCount = 0; @Override public ExitStatus afterStep(StepExecution stepExecution) { if (stepExecution.getStatus() == BatchStatus.FAILED) { Throwable rootCause = getRootCause(stepExecution.getFailureExceptions().get(0)); if (rootCause instanceof SQLIntegrityConstraintViolationException) { duplicateRetryCount++; if (duplicateRetryCount == 1) { // 第一次异常:切换读取方向 fileReader.switchReadDirection(); stepExecution.setExitStatus(ExitStatus.EXECUTING); return ExitStatus.EXECUTING; } else if (duplicateRetryCount == 2) { // 第二次异常:切换下一个文件 boolean hasNextFile = fileReader.switchToNextFile(); if (hasNextFile) { duplicateRetryCount = 0; stepExecution.setExitStatus(ExitStatus.EXECUTING); return ExitStatus.EXECUTING; } else { return ExitStatus.FAILED; } } } } duplicateRetryCount = 0; // 非重复键异常或正常完成,重置计数 return stepExecution.getExitStatus(); } private Throwable getRootCause(Throwable throwable) { while (throwable.getCause() != null) { throwable = throwable.getCause(); } return throwable; } }
关键注意事项
- 逆向读取时需兼容不同编码(如UTF-8、GBK)和换行符(CRLF/LF),避免出现乱码或行内容截断
- 大文件逆向读取建议采用流式处理,避免一次性加载全部内容占用过多内存
- 建议在数据库中维护文件处理状态表,记录每个文件的处理阶段(未处理/正向处理/逆向处理/处理失败),重启时可直接恢复对应状态
- 切换读取方向或文件前,确保当前事务已回滚,避免部分数据残留导致后续异常
内容的提问来源于stack exchange,提问作者Steve Rega
相关产品推荐
相关产品推荐

