SpringBoot-(三十四)快速使用SpringRetry实现重试机制

本文最后更新于:March 27, 2023 pm

SpringBoot框架中有两个非常重要的策略:开箱即用和约定优于配置。其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。

目录

Spring-Retry是Spring提供的一个基于Spring的重试框架,某些场景需要对一些异常情况下的方法进行重试。

依赖

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

启动声明

在启动类上添加@EnableRetry注解表示启用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.tothefor.retrydemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.retry.annotation.EnableRetry;

@EnableRetry
@SpringBootApplication
public class RetryDemoApplication {

public static void main(String[] args) {
SpringApplication.run(RetryDemoApplication.class, args);
}

}

环境准备

一个服务接口及其实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.tothefor.retrydemo;

/**
* @Author DragonOne
* @Date 2023/3/27 13:42
* @墨水记忆 www.tothefor.com
*/

public interface DemoService {
void message();
Integer getAll();
}

实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.tothefor.retrydemo;

import org.springframework.stereotype.Service;

/**
* @Author DragonOne
* @Date 2023/3/27 13:43
* @墨水记忆 www.tothefor.com
*/

@Service
public class DemoServiceImpl implements DemoService {
@Override
public void message() {
System.out.println("MapperImpl-message " + new Date());
}

@Override
public Integer getAll() {
System.out.println("MapperImpl-getAll " + new Date());
return 1;
}
}

处理

以上是正常开发中的具体实现。为了测试重试机制,所以需要添加抛异常:

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
package com.tothefor.retrydemo;

import org.springframework.stereotype.Service;

/**
* @Author DragonOne
* @Date 2023/3/27 13:43
* @墨水记忆 www.tothefor.com
*/

@Service
public class DemoServiceImpl implements DemoService {
@Override
public void message() {
System.out.println("MapperImpl-message " + new Date());
throw new RuntimeException("message Error");
}

@Override
public Integer getAll() {
System.out.println("MapperImpl-getAll " + new Date());
if (true) {
throw new RuntimeException("getAll Error");
}
return 1;
}
}

重试

当抛出异常时,可能是因为网络波动或者其他原因造成的,也许再重试几次就可以了。所以,接下来就通过Spring-Retry实现重试机制。

在需要重试的方法上添加注解@Retryable,它有几个参数:

  • value:指定抛出的异常,只有抛出该异常才会重试。
  • include:和value一样,默认为空,当exclude也为空时,默认所有异常。
  • exclude:指定不处理的异常。
  • maxAttempts:最大重试次数,默认3次。
  • backoff:重试等待策略,默认使用@Backoff,Backoff的参数有:
    • value 重试的间隔,默认为1000(单位毫秒)。
    • multiplier(指定延迟倍数)默认为0.0D。即第一次重试与原本的请求间隔为value的值,第二次的重试与第一次重试的间隔时间为valuemultiplier1,第三次的重试与第二次的间隔为valuemultiplier2,以此类推。

如下实例:

方法处理

1
2
3
4
5
6
@Retryable(value = Exception.class, maxAttempts = 4, backoff = @Backoff(delay = 2000, multiplier = 2))
@Override
public void message() {
System.out.println("MapperImpl-message " + new Date());
throw new RuntimeException("message Error");
}

测试

1
2
3
4
5
6
7
@Autowired
private DemoService demoService;

@Test
void contextLoads() {
demoService.message();
}

输出

1
2
3
4
5
6
MapperImpl-message Mon Mar 27 14:13:30 CST 2023
MapperImpl-message Mon Mar 27 14:13:32 CST 2023
MapperImpl-message Mon Mar 27 14:13:36 CST 2023
MapperImpl-message Mon Mar 27 14:13:44 CST 2023

java.lang.RuntimeException: message Error

第一次为原本的请求,第二次与第一次之间的间隔为我们设置的2秒,第三次和第二次是4秒,然后是8秒。然后因为所有的重试均失败了,所以最后还是抛出了异常。

补偿措施

在上面的测试中,当所有重试均失败后还是抛出了异常。但是,可能我们在有些情况下并不希望抛出异常,所以,Spring-Retry还提供了@Recover注解,用于@Retryable重试失败后处理方法。

示例:

1
2
3
4
5
6
7
8
9
10
11
@Retryable(value = Exception.class, maxAttempts = 4, backoff = @Backoff(delay = 2000, multiplier = 2))
@Override
public void message() {
System.out.println("MapperImpl-message " + new Date());
throw new RuntimeException("message Error");
}

@Recover
public void messageTry(Exception e){ // 异常类型需要和抛出的匹配
System.out.println("MapperImpl-messageTry " + new Date());
}

输出:

1
2
3
4
5
MapperImpl-message Mon Mar 27 14:29:29 CST 2023
MapperImpl-message Mon Mar 27 14:29:31 CST 2023
MapperImpl-message Mon Mar 27 14:29:35 CST 2023
MapperImpl-message Mon Mar 27 14:29:43 CST 2023
MapperImpl-messageTry Mon Mar 27 14:29:43 CST 2023

指定补偿方法

上面只是测试了一个补偿方法,但是如果有多个补偿方法呢?如下示例:

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
36
37
38
39
40
41
42
43
44
45
46
package com.tothefor.retrydemo;

import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

import java.util.Date;

/**
* @Author DragonOne
* @Date 2023/3/27 13:43
* @墨水记忆 www.tothefor.com
*/

@Service
public class DemoServiceImpl implements DemoService {

@Retryable(value = Exception.class, maxAttempts = 4, backoff = @Backoff(delay = 2000, multiplier = 2))
@Override
public void message() {
System.out.println("MapperImpl-message " + new Date());
throw new RuntimeException("message Error");
}

@Recover
public void messageTry(Exception e){
System.out.println("MapperImpl-messageTry " + new Date());
}

@Recover
public void getAllTry(RuntimeException e){
System.out.println("MapperImpl-getAllTry " + new Date());
}

@Retryable(value = RuntimeException.class, maxAttempts = 4, backoff = @Backoff(delay = 2000, multiplier = 2))
@Override
public Integer getAll() {
System.out.println("MapperImpl-getAll " + new Date());
if (true) {
throw new RuntimeException("getAll Error");
}
return 1;
}
}

测试:

1
2
3
4
5
6
7
8
@Autowired
private DemoService demoService;

@Test
void contextLoads() {
demoService.message();
System.out.println(demoService.getAll());
}

输出:

1
2
3
4
5
6
7
8
9
10
11
MapperImpl-message Mon Mar 27 14:32:47 CST 2023
MapperImpl-message Mon Mar 27 14:32:49 CST 2023
MapperImpl-message Mon Mar 27 14:32:53 CST 2023
MapperImpl-message Mon Mar 27 14:33:01 CST 2023
MapperImpl-getAllTry Mon Mar 27 14:33:01 CST 2023
MapperImpl-getAll Mon Mar 27 14:33:01 CST 2023
MapperImpl-getAll Mon Mar 27 14:33:03 CST 2023
MapperImpl-getAll Mon Mar 27 14:33:07 CST 2023
MapperImpl-getAll Mon Mar 27 14:33:15 CST 2023

org.springframework.retry.ExhaustedRetryException: Cannot locate recovery method; nested exception is java.lang.RuntimeException: getAll Error

可以看见,有一个方法并没有走对应的补偿方法,但是,如果将getAllTry()方法修改后一下:

1
2
3
4
5
@Recover
public Integer getAllTry(RuntimeException e){
System.out.println("MapperImpl-getAllTry " + new Date());
return 2;
}

就是正常输出:

1
2
3
4
5
6
7
8
9
10
11
MapperImpl-message Mon Mar 27 14:37:41 CST 2023
MapperImpl-message Mon Mar 27 14:37:43 CST 2023
MapperImpl-message Mon Mar 27 14:37:47 CST 2023
MapperImpl-message Mon Mar 27 14:37:55 CST 2023
MapperImpl-messageTry Mon Mar 27 14:37:55 CST 2023
MapperImpl-getAll Mon Mar 27 14:37:55 CST 2023
MapperImpl-getAll Mon Mar 27 14:37:57 CST 2023
MapperImpl-getAll Mon Mar 27 14:38:01 CST 2023
MapperImpl-getAll Mon Mar 27 14:38:09 CST 2023
MapperImpl-getAllTry Mon Mar 27 14:38:09 CST 2023
2

因为找寻对应的补偿方法是通过方法返回值和异常决定的。当然我们也可以直接指定补偿方法,但是被指定的方法的返回值、方法参数和异常类型都必须和抛出异常的方法保持一致:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Recover
public Integer getAll22222Try(RuntimeException e,int code){
System.out.println("MapperImpl-getAll22222Try " + new Date());
return 4;
}

@Retryable(recover = "getAll22222Try",value = RuntimeException.class, maxAttempts = 4, backoff = @Backoff(delay = 2000, multiplier = 2))
@Override
public Integer getAll(int code) {
System.out.println("MapperImpl-getAll " + new Date());
if (true) {
throw new RuntimeException("getAll Error");
}
return 1;
}

但是,补偿方法中的异常类型可以是@Retryable中异常类型的父类,如:

1
2
getAll22222Try(Exception e,int code)
value = RuntimeException.class

最后

以上只是对Spring-Retry的一个简单使用,如果需要实现更加复杂的,就需要使用RetryTemplate了。Spring-Retry的官方地址:https://github.com/spring-projects/spring-retry