Retry
简介
同熔断器一样,重试组件也提供了注册器,可以通过注册器获取实例来进行重试,同样可以跟熔断器配合使用。
可配置参数
配置参数 | 默认值 | 描述 |
---|---|---|
maxAttempts | 3 | 最大重试次数 |
waitDuration | 500[ms] | 固定重试间隔 |
intervalFunction | numberOfAttempts -> waitDuration | 用来改变重试时间间隔,可以选择指数退避或者随机时间间隔 |
retryOnResultPredicate | result -> false | 自定义结果重试规则,需要重试的返回true |
retryOnExceptionPredicate | throwable -> true | 自定义异常重试规则,需要重试的返回true |
retryExceptions | empty | 需要重试的异常列表 |
ignoreExceptions | empty | 需要忽略的异常列表 |
测试前准备
首先引入maven依赖:
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
<version>1.6.1</version>
</dependency>
resilience4j-spring-boot2 集成了circuitbeaker、retry、bulkhead、ratelimiter、timelimiter 几个模块。
application.yml 配置
retry:
configs:
default:
max-attempts: 3
wait-duration: 10s
enable-exponential-backoff: true # 是否允许使用指数退避算法进行重试间隔时间的计算
exponential-backoff-multiplier: 2 # 指数退避算法的乘数
enable-randomized-wait: false # 是否允许使用随机的重试间隔
randomized-wait-factor: 0.5 # 随机因子
result-predicate: com.example.resilience4j.predicate.RetryOnResultPredicate
retry-exception-predicate: com.example.resilience4j.predicate.RetryOnExceptionPredicate
retry-exceptions:
- com.example.resilience4j.exceptions.BusinessBException
- com.example.resilience4j.exceptions.BusinessAException
- io.github.resilience4j.circuitbreaker.CallNotPermittedException
ignore-exceptions:
- io.github.resilience4j.circuitbreaker.CallNotPermittedException
instances:
backendA:
base-config: default
wait-duration: 5s
backendB:
base-config: default
max-attempts: 2
pplication.yml 可以配置的参数多出了几个enableExponentialBackoff、expontialBackoffMultiplier、enableRandomizedWait、randomizedWaitFactor,分别代表是否允许指数退避间隔时间,指数退避的乘数、是否允许随机间隔时间、随机因子,注意指数退避和随机间隔不能同时启用。
用于监控重试组件状态及事件的工具类
同样为了监控重试组件,写一个工具类:
import io.github.resilience4j.retry.Retry;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class RetryUtil {
/**
* @Description: 获取重试的状态
*/
public static void getRetryStatus(String time, Retry retry){
Retry.Metrics metrics = retry.getMetrics();
long failedRetryNum = metrics.getNumberOfFailedCallsWithRetryAttempt();
long failedNotRetryNum = metrics.getNumberOfFailedCallsWithoutRetryAttempt();
long successfulRetryNum = metrics.getNumberOfSuccessfulCallsWithRetryAttempt();
long successfulNotyRetryNum = metrics.getNumberOfSuccessfulCallsWithoutRetryAttempt();
log.info(time + "state=" + " metrics[ failedRetryNum=" + failedRetryNum +
", failedNotRetryNum=" + failedNotRetryNum +
", successfulRetryNum=" + successfulRetryNum +
", successfulNotyRetryNum=" + successfulNotyRetryNum +
" ]"
);
}
/**
* @Description: 监听重试事件
*/
public static void addRetryListener(Retry retry){
retry.getEventPublisher()
.onSuccess(event -> log.info("服务调用成功:" + event.toString()))
.onError(event -> log.info("服务调用失败:" + event.toString()))
.onIgnoredError(event -> log.info("服务调用失败,但异常被忽略:" + event.toString()))
.onRetry(event -> log.info("重试:第" + event.getNumberOfRetryAttempts() + "次"))
;
}
}
调用方法
还是以之前查询用户列表的服务为例。Retry支持AOP和程序式两种方式的调用.
程序式的调用方法
和CircuitBreaker的调用方式差不多,和熔断器配合使用有两种调用方式,一种是先用重试组件装饰,再用熔断器装饰,这时熔断器的失败需要等重试结束才计算,另一种是先用熔断器装饰,再用重试组件装饰,这时每次调用服务都会记录进熔断器的缓冲环中,需要注意的是,第二种方式需要把CallNotPermittedException放进重试组件的白名单中,因为熔断器打开时重试是没有意义的:
public class CircuitBreakerServiceImpl {
@Autowired
private RemoteServiceConnector remoteServiceConnector;
@Autowired
private CircuitBreakerRegistry circuitBreakerRegistry;
@Autowired
private RetryRegistry retryRegistry;
public List<User> circuitBreakerRetryNotAOP(){
// 通过注册器获取熔断器的实例
CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("backendA");
// 通过注册器获取重试组件实例
Retry retry = retryRegistry.retry("backendA");
CircuitBreakerUtil.getCircuitBreakerStatus("执行开始前:", circuitBreaker);
// 先用重试组件包装,再用熔断器包装
CheckedFunction0<List<User>> checkedSupplier = Retry.decorateCheckedSupplier(retry, remoteServiceConnector::process);
CheckedFunction0<List<User>> chainedSupplier = CircuitBreaker .decorateCheckedSupplier(circuitBreaker, checkedSupplier);
// 使用Try.of().recover()调用并进行降级处理
Try<List<User>> result = Try.of(chainedSupplier).
recover(CallNotPermittedException.class, throwable -> {
log.info("已经被熔断,停止重试");
return new ArrayList<>();
})
.recover(throwable -> {
log.info("重试失败: " + throwable.getLocalizedMessage());
return new ArrayList<>();
});
RetryUtil.getRetryStatus("执行结束: ", retry);
CircuitBreakerUtil.getCircuitBreakerStatus("执行结束:", circuitBreaker);
return result.get();
}
}
AOP式的调用方法
首先在连接器方法上使用@Retry(name="",fallbackMethod="")
注解,其中name是要使用的重试器实例的名称,fallbackMethod是要使用的降级方法:
public RemoteServiceConnector{
@CircuitBreaker(name = "backendA", fallbackMethod = "fallBack")
@Retry(name = "backendA", fallbackMethod = "fallBack")
public List<User> process() throws TimeoutException, InterruptedException {
List<User> users;
users = remoteServic.process();
return users;
}
}
要求和熔断器一致,但是需要注意同时注解重试组件和熔断器的话,是按照第二种方案来的,即每一次请求都会被熔断器记录。
之后直接调用方法:
public class CircuitBreakerServiceImpl {
@Autowired
private RemoteServiceConnector remoteServiceConnector;
@Autowired
private CircuitBreakerRegistry circuitBreakerRegistry;
@Autowired
private RetryRegistry retryRegistry;
public List<User> circuitBreakerRetryAOP() throws TimeoutException, InterruptedException {
List<User> result = remoteServiceConnector.process();
RetryUtil.getRetryStatus("执行结束:", retryRegistry.retry("backendA"));
CircuitBreakerUtil
.getCircuitBreakerStatus("执行结束:", circuitBreakerRegistry.circuitBreaker("backendA"));
return result;
}
}
使用测试
异常A和B在application.yml文件中设定为都需要重试,因为使用第一种方案,所以不需要将CallNotPermittedException设定在重试组件的白名单中,同时为了测试重试过程中的异常是否会被熔断器记录,将异常A从熔断器白名单中去除:
recordExceptions: # 记录的异常
- com.example.resilience4j.exceptions.BusinessBException
- com.example.resilience4j.exceptions.BusinessAException
ignoreExceptions: # 忽略的异常
# - com.example.resilience4j.exceptions.BusinessAException
# ...
resultPredicate: com.example.resilience4j.predicate.RetryOnResultPredicate
retryExceptions:
- com.example.resilience4j.exceptions.BusinessBException
- com.example.resilience4j.exceptions.BusinessAException
- io.github.resilience4j.circuitbreaker.CallNotPermittedException
ignoreExceptions:
# - io.github.resilience4j.circuitbreaker.CallNotPermittedException
使用另一个远程服务接口的实现,将num%4==2的情况返回null,测试根据返回结果进行重试的功能:
public class RemoteServiceImpl implements RemoteService {
private static AtomicInteger count = new AtomicInteger(0);
public List<User> process() {
int num = count.getAndIncrement();
log.info("count的值 = " + num);
if (num % 4 == 1){
throw new BusinessAException("异常A,需要重试");
}
if (num % 4 == 2){
return null;
}
if (num % 4 == 3){
throw new BusinessBException("异常B,需要重试");
}
log.info("服务正常运行,获取用户列表");
// 模拟数据库的正常查询
return repository.findAll();
}
}
同时添加一个类自定义哪些返回值需要重试,设定为返回值为空就进行重试,这样num % 4 == 2时就可以测试不抛异常,根据返回结果进行重试了:
public class RetryOnResultPredicate implements Predicate {
@Override
public boolean test(Object o) {
return o == null ? true : false;
}
}
使用CircuitBreakerServiceImpl中的AOP或者程序式调用方法进行单元测试,循环调用10次:
public class CircuitBreakerServiceImplTest{
@Autowired
private CircuitBreakerServiceImpl circuitService;
@Test
public void circuitBreakerRetryTest() {
for (int i=0; i<10; i++){
// circuitService.circuitBreakerRetryAOP();
circuitService.circuitBreakerRetryNotAOP();
}
}
}
看一下运行结果:
count的值 = 0
服务正常运行,获取用户列表
执行结束: state= metrics[ failedRetryNum=0, failedNotRetryNum=0, successfulRetryNum=0, successfulNotyRetryNum=1 ]
执行结束:state=CLOSED , metrics[ failureRate=-1.0, bufferedCalls=1, failedCalls=0, successCalls=1, maxBufferCalls=5, notPermittedCalls=0 ]
count的值 = 1
重试:第1次
count的值 = 2
重试:第2次
count的值 = 3
服务调用失败:2019-07-09T19:06:59.705+08:00[Asia/Shanghai]: Retry 'backendA' recorded a failed retry attempt. Number of retry attempts: '3', Last exception was: 'com.example.resilience4j.exceptions.BusinessBException: 异常B,需要重试'.
重试失败: 异常B,需要重试
执行结束: state= metrics[ failedRetryNum=1, failedNotRetryNum=0, successfulRetryNum=0, successfulNotyRetryNum=1 ]
执行结束:state=CLOSED , metrics[ failureRate=-1.0, bufferedCalls=2, failedCalls=1, successCalls=1, maxBufferCalls=5, notPermittedCalls=0 ]
这部分结果可以看出来,重试最大次数设置为3结果其实只重试了2次,服务共执行了3次,重试3次后熔断器只记录了1次。而且返回值为null时也确实进行重试了。
服务正常运行,获取用户列表
执行结束: state= metrics[ failedRetryNum=2, failedNotRetryNum=0, successfulRetryNum=0, successfulNotyRetryNum=3 ]
执行结束:state=OPEN , metrics[ failureRate=40.0, bufferedCalls=5, failedCalls=2, successCalls=3, maxBufferCalls=5, notPermittedCalls=0 ]
已经被熔断,停止重试
执行结束: state= metrics[ failedRetryNum=2, failedNotRetryNum=0, successfulRetryNum=0, successfulNotyRetryNum=3 ]
执行结束:state=OPEN , metrics[ failureRate=40.0, bufferedCalls=5, failedCalls=2, successCalls=3, maxBufferCalls=5, notPermittedCalls=1 ]
当熔断之后不会再进行重试。
接下来我修改一下调用服务的实现:
public class RemoteServiceImpl implements RemoteService {
private static AtomicInteger count = new AtomicInteger(0);
public List<User> process() {
int num = count.getAndIncrement();
log.info("count的值 = " + num);
if (num % 4 == 1){
throw new BusinessAException("异常A,需要重试");
}
if (num % 4 == 3){
return null;
}
if (num % 4 == 2){
throw new BusinessBException("异常B,需要重试");
}
log.info("服务正常运行,获取用户列表");
// 模拟数据库的正常查询
return repository.findAll();
}
}
将num%4==2变成异常B,num%4==3变成返回null,看一下最后一次重试返回值为null属于重试成功还是重试失败。
运行结果如下:
count的值 = 0
服务正常运行,获取用户列表
执行结束: state= metrics[ failedRetryNum=0, failedNotRetryNum=0, successfulRetryNum=0, successfulNotyRetryNum=1 ]
执行结束:state=CLOSED , metrics[ failureRate=-1.0, bufferedCalls=1, failedCalls=0, successCalls=1, maxBufferCalls=5, notPermittedCalls=0 ]
count的值 = 1
重试:第1次
count的值 = 2
重试:第2次
count的值 = 3
服务调用成功:2019-07-09T19:17:35.836+08:00[Asia/Shanghai]: Retry 'backendA' recorded a successful retry attempt. Number of retry attempts: '3', Last exception was: 'com.example.resilience4j.exceptions.BusinessBException: 异常B,需要重试'.
如上可知如果最后一次重试不抛出异常就算作重试成功,不管结果是否需要继续重试。
作者:I讨厌鬼I
链接:https://www.jianshu.com/p/5531b66b777a
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
更新时间:2024-10-26 16:27