You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 14:15:49