1. 概述

默认情况下,Spring Batch 作业在执行过程中只要遇到异常就会立即失败。但在实际生产环境中,我们往往希望系统具备更强的容错能力,尤其是面对临时性故障(如网络抖动、数据库锁冲突等)时,能够自动重试而不是直接挂掉。

本文将带你掌握如何在 Spring Batch 中配置重试逻辑,提升批处理任务的稳定性与健壮性。✅

2. 场景示例

假设我们有一个批处理任务,用于处理如下格式的 CSV 输入文件:

username, userid, transaction_date, transaction_amount
sammy, 1234, 31/10/2015, 10000
john, 9999, 3/12/2015, 12321

任务流程如下:

  1. 读取每条交易记录;
  2. 调用外部 REST 接口,根据 userid 查询用户的 agepostCode
  3. 将补充信息写入最终的 XML 报表。

核心处理逻辑在 ItemProcessor 中实现:

public class RetryItemProcessor implements ItemProcessor<Transaction, Transaction> {
    
    @Override
    public Transaction process(Transaction transaction) throws IOException {
        log.info("RetryItemProcessor, attempting to process: {}", transaction);
        HttpResponse response = fetchMoreUserDetails(transaction.getUserId());
        // 解析 age 和 postCode 并更新 transaction
        ...
        return transaction;
    }
    ...
}

最终生成的 XML 输出示例:

<transactionRecord>
    <transactionRecord>
        <amount>10000.0</amount>
        <transactionDate>2015-10-31 00:00:00</transactionDate>
        <userId>1234</userId>
        <username>sammy</username>
        <age>10</age>
        <postCode>430222</postCode>
    </transactionRecord>
    ...
</transactionRecord>

3. 为 ItemProcessor 添加重试机制

如果在调用 REST 接口时因网络延迟导致超时(ConnectTimeoutException),整个作业就会中断——这显然不合理。我们更希望对这类可恢复异常进行自动重试。

✅ 解决方案:在 Step 配置中启用容错机制,并指定重试策略。

@Bean
public Step retryStep(
    ItemProcessor<Transaction, Transaction> processor,
    ItemWriter<Transaction> writer) throws ParseException {
    return stepBuilderFactory
      .get("retryStep")
      .<Transaction, Transaction>chunk(10)
      .reader(itemReader(inputCsv))
      .processor(processor)
      .writer(writer)
      .faultTolerant()  // 启用容错(必须先开启)
      .retryLimit(3)    // 最多重试 3 次
      .retry(ConnectTimeoutException.class)           // 指定哪些异常触发重试
      .retry(DeadlockLoserDataAccessException.class)
      .build();
}

⚠️ 注意要点:

  • faultTolerant() 是前提,不开启则后续的 retry 配置无效;
  • retryLimit(n) 定义单个 item 的最大重试次数(含首次执行);
  • retry(Class) 可多次调用,添加多个需要重试的异常类型;
  • 重试是按 item 粒度进行的,不会影响整个 chunk。

4. 重试机制测试验证

✅ 场景一:前两次失败,第三次成功

我们模拟 REST 接口前两次调用超时,第三次恢复正常:

@Test
public void whenEndpointFailsTwicePasses3rdTime_thenSuccess() throws Exception {
    FileSystemResource expectedResult = new FileSystemResource("expected-output.xml");
    FileSystemResource actualResult = new FileSystemResource("actual-output.xml");

    when(httpResponse.getEntity())
      .thenReturn(new StringEntity("{ \"age\":10, \"postCode\":\"430222\" }"));
 
    // 前两次抛出超时异常,第三次返回正常响应
    when(httpClient.execute(any()))
      .thenThrow(new ConnectTimeoutException("Timeout count 1"))
      .thenThrow(new ConnectTimeoutException("Timeout count 2"))
      .thenReturn(httpResponse);

    JobExecution jobExecution = jobLauncherTestUtils.launchJob(defaultJobParameters());
    JobInstance actualJobInstance = jobExecution.getJobInstance();
    ExitStatus actualJobExitStatus = jobExecution.getExitStatus();

    assertThat(actualJobInstance.getJobName(), is("retryBatchJob"));
    assertThat(actualJobExitStatus.getExitCode(), is("COMPLETED"));
    AssertFile.assertFileEquals(expectedResult, actualResult);
}

日志输出清晰地展示了重试过程:

19:06:57.742 [main] INFO  o.s.batch.core.job.SimpleStepHandler - Executing step: [retryStep]
19:06:57.758 [main] INFO  o.b.batch.service.RetryItemProcessor - Attempting to process user with id=1234
19:06:57.758 [main] INFO  o.b.batch.service.RetryItemProcessor - Attempting to process user with id=1234
19:06:57.758 [main] INFO  o.b.batch.service.RetryItemProcessor - Attempting to process user with id=1234
19:06:57.758 [main] INFO  o.b.batch.service.RetryItemProcessor - Attempting to process user with id=9999
19:06:57.773 [main] INFO  o.s.batch.core.step.AbstractStep - Step: [retryStep] executed in 31ms

可以看到,id=1234 的记录被处理了三次(前两次失败,第三次成功),之后才继续处理下一条。

❌ 场景二:重试耗尽,作业最终失败

如果接口持续不可用,重试次数用尽后作业应正常标记为失败:

@Test
public void whenEndpointAlwaysFail_thenJobFails() throws Exception {
    when(httpClient.execute(any()))
      .thenThrow(new ConnectTimeoutException("Endpoint is down"));

    JobExecution jobExecution = jobLauncherTestUtils.launchJob(defaultJobParameters());
    JobInstance actualJobInstance = jobExecution.getJobInstance();
    ExitStatus actualJobExitStatus = jobExecution.getExitStatus();

    assertThat(actualJobInstance.getJobName(), is("retryBatchJob"));
    assertThat(actualJobExitStatus.getExitCode(), is("FAILED"));
    assertThat(actualJobExitStatus.getExitDescription(),
      containsString("org.apache.http.conn.ConnectTimeoutException"));
}

此时,系统会对第一条记录执行 3 次重试(共 4 次尝试),最终仍失败,导致整个 Step 失败。

5. XML 配置方式

除了 Java Config,Spring Batch 也支持 XML 方式配置重试逻辑,适用于传统项目或偏好 XML 的团队:

<batch:job id="retryBatchJob">
    <batch:step id="retryStep">
        <batch:tasklet>
            <batch:chunk reader="itemReader" 
                         writer="itemWriter"
                         processor="retryItemProcessor" 
                         commit-interval="10"
                         retry-limit="3">
                <batch:retryable-exception-classes>
                    <batch:include class="org.apache.http.conn.ConnectTimeoutException"/>
                    <batch:include class="org.springframework.dao.DeadlockLoserDataAccessException"/>
                </batch:retryable-exception-classes>
            </batch:chunk>
        </batch:tasklet>
    </batch:step>
</batch:job>

📌 对比 Java 配置,XML 中的关键属性:

  • retry-limit="3" → 相当于 .retryLimit(3)
  • <batch:include> → 相当于 .retry(Exception.class)

6. 总结

本文通过一个实际场景,演示了如何在 Spring Batch 中配置重试机制来应对临时性故障:

  • 使用 .faultTolerant() 开启容错;
  • 通过 .retry().retryLimit() 精确控制重试行为;
  • 支持 Java 和 XML 两种配置方式;
  • 重试基于 item 粒度,不影响整体 chunk 提交;
  • 建议结合单元测试验证重试逻辑是否按预期工作。

💡 踩坑提醒:不要对所有异常都开启重试!应只针对幂等操作明确可恢复的异常(如网络超时、死锁等)进行重试,避免重复扣款、数据重复等问题。

完整示例代码已上传至 GitHub:https://github.com/example/spring-batch-retry-demo


原始标题:Configuring Retry Logic in Spring Batch