Spring Retry 指北
2025-01-30 13:58:51    1.2k 字   
This post is also available in English and alternative languages.

Spring Retry 是一套轻量级的重试框架,它提供了一组用于处理可重试异常的工具和策略。Spring Retry 旨在简化开发人员处理因网络延迟、资源暂时不可用或其他瞬时故障导致的异常操作。

Spring Retry 的整体流程如下:

spring-retry整体流程(图片来源:https://www.jianshu.com/p/3dc4f9bbe605)

1. RetryTemplate

RetryTemplate 是 Spring Retry 框架的核心组件,它封装了重试操作的基本流程。通过 RetryTemplate,可以指定重试策略、回退策略、监听器等,以灵活地控制重试行为。

主要功能包括:

  • 执行重试操作
  • 指定重试策略
  • 指定回退策略
  • 注册重试监听器
  • 获取重试上下文

2. RetryCallback 和 RecoveryCallback

RetryCallback 接口用于封装业务逻辑代码。在 RetryTemplate 执行重试操作时,会回调 RetryCallback 接口中的 doInRetry() 方法来执行业务逻辑。如果 doInRetry() 方法抛出异常,则 RetryTemplate 会根据指定的重试策略进行重试。

RecoveryCallback 接口用于封装兜底处理逻辑。当 RetryTemplate 经过指定的重试次数后仍然无法成功执行业务逻辑时,会回调 RecoveryCallback 接口中的 recover() 方法来执行兜底处理。


3. 重试策略(RetryPolicy)

重试策略决定了 RetryTemplate 如何进行重试操作。Spring Retry 提供了多种重试策略,包括:

  • SimpleRetryPolicy:固定次数重试策略,默认重试最大次数为 3 次。
  • NeverRetryPolicy:不允许重试,只允许调用 RetryCallback 接口一次。
  • AlwaysRetryPolicy:无限重试,直到成功。
  • TimeoutRetryPolicy:超时时间重试策略,在指定的超时时间内允许重试。
  • CircuitBreakerRetryPolicy:具有熔断功能的重试策略,可以防止因频繁重试导致的资源耗尽。

4. 回退策略

回退策略决定了 RetryTemplate 在重试之间等待的时间间隔。Spring Retry 提供了多种回退策略,包括:

  • FixedBackOffPolicy:固定时间回退策略,每次重试之间等待固定的时间间隔。
  • ExponentialBackOffPolicy:指数退避策略,每次重试之间等待的时间间隔会呈指数增长。
  • ExponentialRandomBackOffPolicy:指数随机退避策略,每次重试之间等待的时间间隔会在一个随机范围内呈指数增长。

5. 注解方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Slf4j
@Service
public class RefuseService {
@Retryable(
value = {Exception.class}, // 指定需要重试的异常类型
maxAttempts = 3, // 最大重试次数
backoff = @Backoff(delay = 1000, multiplier = 2) // 重试间隔和退避策略
)
public void testRetryAnnotation() {
log.info("RefuseService.testRetryAnnotation() 调用,{}", DateUtil.formatDateTime(new Date()));
System.out.println();
try {
int i = 1 / 0;
} catch (Exception e) {
log.error("errMsg:[{}]", e.getMessage());
throw e;
}
}

@Recover
public void recover(Exception e) {
log.error("RefuseService.testRetryAnnotation(),重试仍然失败了,执行兜底逻辑");
log.error(e.getMessage(), e);
}
}
1
2
3
4
5
6
7
8
9
10
11
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class ApplicationSpringBootExampleTest20 {
@Resource
private RefuseService refuseService;
@Test
public void test() {
refuseService.testRetryAnnotation();
}
}

注意:需要在启动类加上@EnableRetry注解。


6. 模板方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Slf4j
@Service
public class RefuseService {

public void testRetryTemplate() {
RetryTemplate retryTemplate = RetryTemplate.builder().retryOn(Exception.class).build();

SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(5);
retryTemplate.setRetryPolicy(retryPolicy);

ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
backOffPolicy.setInitialInterval(1000);
backOffPolicy.setMultiplier(2);
retryTemplate.setBackOffPolicy(backOffPolicy);

retryTemplate.execute(
retryContext -> {
log.info("RetryCount:{}", retryContext.getRetryCount());
log.info("RefuseService.testRetryTemplate() 调用,date:{}", DateUtil.formatDateTime(new Date()));
try {
int i = 1 / 0;
} catch (Exception e) {
log.error("errMsg:[{}]", e.getMessage());
throw e;
}
return StringUtils.EMPTY;
},
retryContext -> {
log.error("RefuseService.testRetryTemplate() 重试仍然失败了,errMsg:{}",
retryContext.getLastThrowable().getMessage(), retryContext.getLastThrowable());
return StringUtils.EMPTY;
});
}
}
  • line 8~10:设置简单重试策略,最大重试次数为5次,RetryContext.getRetryCount()的值是从 0 开始的,如果maxAttempts设置为 5 的话,那就最大值为 5-1=4。
1
2
3
4
5
6
7
8
9
10
11
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class ApplicationSpringBootExampleTest20 {
@Resource
private RefuseService refuseService;
@Test
public void test2() {
refuseService.testRetryTemplate();
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
c.y.example20.service.RefuseService      : RetryCount:0
c.y.example20.service.RefuseService : RefuseService.testRetryTemplate() 调用,date:2024-04-17 13:10:39
c.y.example20.service.RefuseService : errMsg:[/ by zero]

c.y.example20.service.RefuseService : RetryCount:1
c.y.example20.service.RefuseService : RefuseService.testRetryTemplate() 调用,date:2024-04-17 13:10:40
c.y.example20.service.RefuseService : errMsg:[/ by zero]

c.y.example20.service.RefuseService : RetryCount:2
c.y.example20.service.RefuseService : RefuseService.testRetryTemplate() 调用,date:2024-04-17 13:10:42
c.y.example20.service.RefuseService : errMsg:[/ by zero]

c.y.example20.service.RefuseService : RetryCount:3
c.y.example20.service.RefuseService : RefuseService.testRetryTemplate() 调用,date:2024-04-17 13:10:46
c.y.example20.service.RefuseService : errMsg:[/ by zero]

c.y.example20.service.RefuseService : RetryCount:4
c.y.example20.service.RefuseService : RefuseService.testRetryTemplate() 调用,date:2024-04-17 13:10:54
c.y.example20.service.RefuseService : errMsg:[/ by zero]

c.y.example20.service.RefuseService : RefuseService.testRetryTemplate() 重试仍然失败了,errMsg:/ by zero

java.lang.ArithmeticException: / by zero
at com.yxcheng.example20.service.RefuseService.lambda$testRetryTemplate$0(RefuseService.java:74)
...

7. 小结

注解方式使用方便,代码量较少。不过个人认为有个缺点,就是没有其他的方式能在 @Recover 注解中区分不同的兜底处理方法。兜底处理方法的区分主要是基于异常类型的。毕竟很多时候,不同的业务逻辑需要执行不同的兜底逻辑。而模板方式则可以设置不同兜底逻辑。


8. 依赖

1
2
3
4
5
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.3.4</version>
</dependency>