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
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

作者:Jeebiz  创建时间:2020-11-05 00:10
 更新时间:2023-12-22 21:08