背景
和公司内部对接rpc接口,可能存在调用失败的情况,此时就需要重新重试,保证业务顺利进行,重试失败需要对失败结果进行处理。
Spring的重试机制
Spring Retry 为应用程序提供了重试机制,针对客户调用过程中发生的异常进行重试处理。而不需要再去手动处理。
依赖
我自己新建了一个spring boot工程。依赖如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!--引入spring boot-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.example</groupId>
<artifactId>WorkTest</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--tomcat集成-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>
<!--dubbo依赖,可以让程序一直运行,不会启动后就停止-->
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>0.1.0</version>
</dependency>
<!--spirng boot @SpringBootApplication等启动自注解依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.3.9.RELEASE</version>
</dependency>
<!--Java常用工具类依赖-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.3.1</version>
</dependency>
<!--Spring Retry 重试机制依赖-->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.2.2.RELEASE</version>
</dependency>
<!--lombo工具依赖-》log、Data等-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--测试依赖-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
</dependency>
</dependencies>
<!--maven插件-->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>application.yml</include>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
</build>
</project>
启动类实现
启动类:
package com.anzhi.worktest;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @Package: com.anzhi.worktest
* @ClassName: WorkTestApplication
* @Author: AZ
* @CreateTime: 2021/11/7 17:20
* @Description:
*/
@SpringBootApplication(scanBasePackages = "com.anzhi.worktest.*")
public class WorkTestApplication {
public static void main(String[] args) {
System.setProperty("workTest", "workTest");
SpringApplication.run(WorkTestApplication.class, args);
}
}
测试工具类
实现一个随机工具类根据不同的数字抛出不同异常
@Slf4j
public class RetryDemoTaskUtil {
/**
* 重试方法
* @param param
* @return
*/
public static boolean retyrTask(String param){
log.info("收到请求参数: {}", param);
int i = RandomUtils.nextInt(0, 11);
log.info("随机生成的参数: {}", i);
if(i == 0){
log.info("为0, 抛出参数异常");
throw new IllegalArgumentException("参数异常");
}else if(i ==1){
log.info("为1, 返回true");
return true;
}else if(i == 2){
log.info("为2,返回false");
return false;
}else{
// 为其他
log.info("大于2, 抛出自定义异常");
throw new RemoteAccessException("大于2, 抛出远程访问异常");
}
}
}
测试类
@Slf4j
public class SpringRetryTemplateTest {
/**
* 重试时间间隔
*/
private long fixedPeriodTime = 1000L;
/**
* 最大重试次数, 默认为3
*/
private int maxRetryTimes = 3;
/**
* 表示那些异常需要重试, key表示异常的字节码, value为true表示需要重试
*/
private Map<Class<? extends Throwable>, Boolean> exceptionMap = new HashMap<>();
@Test
public void testRetry(){
// 设置重试回退策略, 主要设置重试时间间隔
FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
backOffPolicy.setBackOffPeriod(fixedPeriodTime);
// 设置重试策略,主要设置重试次数, 以及什么异常进行重试
exceptionMap.put(RemoteAccessException.class, true);
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(maxRetryTimes, exceptionMap);
// 构建重试模板实列
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy(retryPolicy);
retryTemplate.setBackOffPolicy(backOffPolicy);
Boolean excute = retryTemplate.execute(
retryContext -> {
boolean b = RetryDemoTaskUtil.retyrTask("abc");
log.info("调用结果: {}", b);
return b;
},
retryContext -> {
log.info("已经达到最大重试次数或抛出了不重试的异常");
return false;
});
log.info("执行结果: {}", excute);
}
}
运行结果:
19:08:09.740 [main] DEBUG org.springframework.retry.support.RetryTemplate - Retry: count=0
19:08:09.747 [main] INFO com.anzhi.worktest.Retry.util.RetryDemoTaskUtil - 收到请求参数: abc
19:08:09.754 [main] INFO com.anzhi.worktest.Retry.util.RetryDemoTaskUtil - 随机生成的参数: 0
19:08:09.755 [main] INFO com.anzhi.worktest.Retry.util.RetryDemoTaskUtil - 为0, 抛出参数异常
19:08:09.758 [main] DEBUG org.springframework.retry.support.RetryTemplate - Checking for rethrow: count=1
19:08:09.758 [main] DEBUG org.springframework.retry.support.RetryTemplate - Retry failed last attempt: count=1
19:08:09.758 [main] INFO com.anzhi.worktest.Retry.demo.SpringRetryTemplateTest - 已经达到最大重试次数或抛出了不重试的异常
19:08:09.758 [main] INFO com.anzhi.worktest.Retry.demo.SpringRetryTemplateTest - 执行结果: false
观察结果可以看到有自动重试
分析
RetryTemplate承担了重试任务执行者的角色。包含了SimpleRetryPolicy——重试策略(设置重试上限,重试的根源实体),FixedBackOffPolicy——固定的回退策略(设置执行重试回退的时间间隔)
RetryTemplate通过excute提交执行操作,需要准备RetryCallback 和RecoveryCallback 两个类实例,前者对应重试回调逻辑实例,包含正常的逻辑操作。后者实现的是整个重试机制执行完的操作实例
重试策略
重试策略也分多种,不单单只有一种
NeverRetryPolicy:只允许调用RetryCallback一次,不允许重试
AlwaysRetryPolicy:允许无限重试,直到成功,此方式逻辑不当会导致死循环
SimpleRetryPolicy:固定次数重试策略,默认重试最大次数为3次,RetryTemplate默认使用的策略
TimeoutRetryPolicy:超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试
ExceptionClassifierRetryPolicy:设置不同异常的重试策略,类似组合重试策略,区别在于这里只区分不同异常的重试
CircuitBreakerRetryPolicy:有熔断功能的重试策略,需设置3个参数openTimeout、resetTimeout和delegate
CompositeRetryPolicy:组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许即可以重试,悲观组合重试策略是指只要有一个策略不允许即可以重试,但不管哪种组合方式,组合中的每一个策略都会执行
重试回退策略
大白话讲就是失败你要立即做出处理还是等一会儿处理。Spring Retry默认是立即重试。如果需要配置等待一段时间则需要指定回退策略BackoffRetryPolicy。
NoBackOffPolicy:无退避算法策略,每次重试时立即重试
FixedBackOffPolicy:固定时间的退避策略,需设置参数sleeper和backOffPeriod,sleeper指定等待策略,默认是Thread.sleep,即线程休眠,backOffPeriod指定休眠时间,默认1秒
UniformRandomBackOffPolicy:随机时间退避策略,需设置sleeper、minBackOffPeriod和maxBackOffPeriod,该策略在[minBackOffPeriod,maxBackOffPeriod之间取一个随机休眠时间,minBackOffPeriod默认500毫秒,maxBackOffPeriod默认1500毫秒
ExponentialBackOffPolicy:指数退避策略,需设置参数sleeper、initialInterval、maxInterval和multiplier,initialInterval指定初始休眠时间,默认100毫秒,maxInterval指定最大休眠时间,默认30秒,multiplier指定乘数,即下一次休眠时间为当前休眠时间*multiplier
ExponentialRandomBackOffPolicy:随机指数退避策略,引入随机乘数可以实现随机乘数回退
以上是基于Spring-Retry 的普通使用方式。还有基于 Spring Boot 集成的使用方式。使用注解。
Spring Retry注解方式
另一种比较简单的重试机制是基于注解的方式
添加依赖:
<!--Spring Retry 重试机制依赖-->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.2.2.RELEASE</version>
</dependency>
初始化打印
切面打印日志
@Slf4j
@SpringBootTest(classes = WorkTestApplication.class)
@RunWith(SpringRunner.class)
public class SpringRetryAnnotation {
@Before
public void init(){
log.info("-------------测试开始-----------------");
}
@After
public void after(){
log.info("-------------测试结束-----------------");
}
}
Service层测试
Service层代码,调用重试的逻辑方法。使用注解实现重试机制
@Service
@Slf4j
public class SpringRetryAnnoService {
@Retryable(value = {RemoteAccessException.class, IllegalArgumentException.class}, maxAttempts = 3, backoff = @Backoff(delay = 2000L, multiplier = 2))
public boolean call(String param){
return RetryDemoTaskUtil.retyrTask("abc");
}
@Recover
public boolean recover(Exception e, String param){
log.error("达到最大重试次数,或抛出了一个没有指定进行重试的异常 {}", e);
return false;
}
}
测试类
@Slf4j
public class SpringRetryAnnoTest extends SpringRetryAnnotation {
@Autowired
private SpringRetryAnnoService springRetryAnnoService;
@Test
public void retyr(){
boolean result = springRetryAnnoService.call("abc");
log.info("----结果是: {} --", result);
}
}
测试结果:
2021-11-08 22:55:45.484 INFO 20332 --- [main] c.a.w.r.common.SpringRetryAnnotation : -------------测试开始-----------------
2021-11-08 22:55:45.529 INFO 20332 --- [main] c.a.w.retry.util.RetryDemoTaskUtil : 收到请求参数: abc
2021-11-08 22:55:45.534 INFO 20332 --- [main] c.a.w.retry.util.RetryDemoTaskUtil : 随机生成的参数: 3
2021-11-08 22:55:45.535 INFO 20332 --- [main] c.a.w.retry.util.RetryDemoTaskUtil : 大于2, 抛出自定义异常
2021-11-08 22:55:47.549 INFO 20332 --- [main] c.a.w.retry.util.RetryDemoTaskUtil : 收到请求参数: abc
2021-11-08 22:55:47.550 INFO 20332 --- [main] c.a.w.retry.util.RetryDemoTaskUtil : 随机生成的参数: 3
2021-11-08 22:55:47.550 INFO 20332 --- [main] c.a.w.retry.util.RetryDemoTaskUtil : 大于2, 抛出自定义异常
2021-11-08 22:55:51.552 INFO 20332 --- [main] c.a.w.retry.util.RetryDemoTaskUtil : 收到请求参数: abc
2021-11-08 22:55:51.552 INFO 20332 --- [main] c.a.w.retry.util.RetryDemoTaskUtil : 随机生成的参数: 6
2021-11-08 22:55:51.552 INFO 20332 --- [main] c.a.w.retry.util.RetryDemoTaskUtil : 大于2, 抛出自定义异常
2021-11-08 22:55:51.563 ERROR 20332 --- [main] c.a.w.r.server.SpringRetryAnnoService : 达到最大重试次数,或抛出了一个没有指定进行重试的异常 {}
Guava-Retry重试框架
依赖
<!--重试框架之Guava-Retry依赖-->
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
</dependency>
重试方法:
@Slf4j
public class RetryGuavaDemoTask {
/**
* 重试方法
*/
public static boolean retryTask(String param){
log.info("收到请求参数: {}", param);
int result = RandomUtils.nextInt(0, 11);
log.info("随机生成的数: {}", result);
if(result < 2){
log.info("小于2, 抛出参数异常");
throw new IllegalArgumentException("参数异常");
}else if(result < 5){
log.info("小于5, 返回true");
return true;
}else if(result < 7){
log.info("小于7, 返回false");
return false;
}else{
log.info("大于7, 抛出自定义异常");
throw new RemoteAccessException("大于7, 抛出自定义异常");
}
}
}
测试方法
@Slf4j
public class GuavaRetryTest {
@Test
public void fun01(){
// RetryerBuilder 构建重试实例 retryer,可以设置重试源且可以支持多个重试源,可以配置重试次数或重试超时时间,
// 以及可以配置等待时间间隔
Retryer<Boolean> retryer = RetryerBuilder.<Boolean> newBuilder()
.retryIfExceptionOfType(RemoteAccessException.class) //设置异常重试源
.retryIfResult(res -> res == false) //设置根据结果重试
.withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS)) // 设置等待时间间隔
.withStopStrategy(StopStrategies.stopAfterAttempt(3)) //设置最大重试次数
.build();
try {
retryer.call(() -> RetryGuavaDemoTask.retryTask("abc"));
}catch (Exception e){
log.info("重试异常: {}", e);
}
}
}
分析
指定异常:
-
RetryBuilder 是一个Factory创建者,可以定制设置重试源并且可以支持多个重试源,可以配置重试次数和超时时间,以及可以配置等待时间间隔,创建重试者Retryer实例。
-
RetryerBuilder 的重试源支持Exception异常对象和自定义断言对象,通过retryIfException和retryIfResult设置,同时支持多个且能兼容。
-
retryIfException: 抛出runtime异常、checked异常时都会重试,但是抛出error不会重试
-
retryIfRuntimeException: retryIfRuntimeException 只会在抛 runtime 异常的时候才重试,checked 异常和error 都不重试。
-
retryIfExceptionOfType: retryIfExceptionOfType 允许我们只在发生特定异常的时候才重试,比如NullPointerException 和 IllegalStateException 都属于 runtime 异常,也包括自定义的error。
如: retryIfExceptionOfType(Error.class)// 只在抛出error重试; 或者通过Predicate实现: .retryIfException(Predicates.or(Predicates.instanceOf(NullPointerException.class),
Predicates.instanceOf(IllegalStateException.class)))
根据返回结果重试
- RetryifResult: 可以根据Callable方法的返回值进行重试
如上述代码所示:
.retryIfResult(res -> res == false) //设置根据结果重试
retryer.call(() -> RetryGuavaDemoTask.retryTask("abc"));'
RetryListener
当发生重试之后,假如需要做一些额外的处理动作,比如log异常,那么可以使用RetryListener。 每次重试后,guava-retrying会自动回调注册的监听。可以注册多个RetryListener,会按照注册顺序一次调用。如:
.withRetryListener(new RetryListener {
@Override
public <T> void onRetry(Attempt<T> attempt) {
logger.error("第【{}】次调用失败" , attempt.getAttemptNumber());
}
})
主要接口
| 接口 | 描述 | 备注 |
|---|---|---|
| Attemp | 执行一次任务 | |
| AttemptTimeLimiter | 单次任务执行时间限制 | 如果单次任务执行超时,则终止执行当前任务 |
| BlockStrategies | 任务阻塞策略 | 通俗的讲就是当前任务执行完,下次任务还没开始这段时间做什么),默认策略为:BlockStrategies.THREAD_SLEEP_STRATEGY |
| RetryException | 重试异常 | |
| RetryListener | 自定义重试监听器 | 可以用于异步记录错误日志 |
| StopStrategy | 停止重试策略 | |
| WaitStrategy | 等待时长策略 | (控制时间间隔),返回结果为下次执行时长 |
StopStrategy 停止策略
提供三种:
-
StopAfterDelayStrategy: 设定一个最长允许的执行时间;比如设定最长执行10s,无论任务执行次数,只要重试的时候超出了最长时间,则任务终止,并返回重试异常RetryException;
-
NeverStopStrategy: 不停止,用于需要一直轮训知道返回期望结果的情况;
-
StopAfterAttemptStrategy: 设定最大重试次数,如果超出最大重试次数则停止重试,并返回重试异常;
WaitStrategy等待策略
-
FixedWaitStrategy: 固定等待时长策略;
-
RandomWaitStrategy: 随机等待时长策略(可以提供一个最小和最大时长,等待时长为其区间随机值)
-
IncrementingWaitStrategy: 递增等待时长策略(提供一个初始值和步长,等待时间随重试次数增加而增加)
-
ExponentialWaitStrategy: 指数等待时长策略;
-
ExceptionWaitStrategy: 异常时长等待策略;
-
CompositeWaitStrategy: 复合时长等待策略;
总结
spring-retry 和guava-retry 工具都是线程安全的重试,能够支持并发业务场景的重试逻辑正确性。但是可以很明显的可以看出guava-retry使用具有更高的灵活性,能根据方法返回值来判断是否重试,而Spring-retry只能根据抛出的异常来进行重试。
Spring-Retry 使用注意事项
- 异常类型需要与Recover方法参数类型保持一致,且重试方法第一个参数必须为Throwable或其子类,否则找不到重试回调的方法;
- recover方法返回值需要与重试方法返回值保证一致
参考文章:
blog.csdn.net/zzzgd_666/a… blog.csdn.net/ryo10607324… blog.csdn.net/qq_35152236… blog.csdn.net/qq_39914581…