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: 2pplication.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
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
更新时间:2025-07-19 11:45