• 如何使用Guava Retry组件实现接口重试机制?
  • 发布于 1周前
  • 66 热度
    0 评论
  • LoveC
  • 1 粉丝 48 篇博客
  •   
背景
在众多业务场景中,为了消除系统内的不稳定因素及逻辑错误,确保尽可能地达到预期结果,重试机制显得尤为重要。特别是在调用远程服务时,由于服务器响应延迟或网络问题,使得我们无法及时获得所需结果,甚至完全收不到响应。面对这种情况,实施一种高效且优雅的重试策略能够显著提高获取预期响应的概率。

重试机制不仅有助于应对短暂的技术故障,还能增强系统的稳定性和可靠性。通过合理设置重试次数、间隔时间和条件判断等参数,可以在不影响用户体验的前提下,自动处理一些非永久性错误。例如,在网络连接不稳定的情况下,适当增加重试次数并延长每次尝试之间的等待时间,往往能够有效克服瞬时性的网络波动,从而顺利完成服务调用。

正文
Spring Retry不做介绍, 因为只支持在抛出异常时进行重试

Guava Retry
基于Google Guava库开发的一个轻量级重试组件。它提供了一种通用的方法来重试任意Java代码片段,具备特定的停止、重试和异常处理能力。通过灵活的配置选项,开发者可以轻松地为各种场景定制重试策略,如HTTP请求、数据库操作等。

核心概念
Retryer:重试器对象,负责管理整个重试流程。
WaitStrategy:等待策略,定义了两次重试之间的等待时间。
StopStrategy:停止策略,决定了何时停止重试。
RetryListener:重试监听器,用于监听每次重试的过程,可用于记录日志或发送通知。
Predicate:断言函数,用于决定是否需要重试。
RetryerBuilder重试器构建:
/**
 * 构建一个重试器时,指定当发生任何异常时进行重试
 *  堆代码 duidaima.com
 * @return RetryerBuilder<V> 返回当前的重试器构建器实例,用于链式调用
 */
public RetryerBuilder<V> retryIfException() {
    this.rejectionPredicate = Predicates.or(this.rejectionPredicate, new ExceptionClassPredicate(Exception.class));
    return this;
}

/**
 * 构建一个重试器时,指定当发生运行时异常时进行重试
 * 
 * @return RetryerBuilder<V> 返回当前的重试器构建器实例,用于链式调用
 */
public RetryerBuilder<V> retryIfRuntimeException() {
    this.rejectionPredicate = Predicates.or(this.rejectionPredicate, new ExceptionClassPredicate(RuntimeException.class));
    return this;
}

/**
 * 构建一个重试器时,指定当发生特定类型的异常时进行重试
 * 
 * @param exceptionClass 不可为空,指定的异常类型
 * @return RetryerBuilder<V> 返回当前的重试器构建器实例,用于链式调用
 */
public RetryerBuilder<V> retryIfExceptionOfType(@Nonnull Class<? extends Throwable> exceptionClass) {
    Preconditions.checkNotNull(exceptionClass, "exceptionClass may not be null");
    this.rejectionPredicate = Predicates.or(this.rejectionPredicate, new ExceptionClassPredicate(exceptionClass));
    return this;
}

/**
 * 构建一个重试器时,指定当发生的异常满足给定的谓词时进行重试
 * 
 * @param exceptionPredicate 不可为空,用于判断异常的谓词
 * @return RetryerBuilder<V> 返回当前的重试器构建器实例,用于链式调用
 */
public RetryerBuilder<V> retryIfException(@Nonnull Predicate<Throwable> exceptionPredicate) {
    Preconditions.checkNotNull(exceptionPredicate, "exceptionPredicate may not be null");
    this.rejectionPredicate = Predicates.or(this.rejectionPredicate, new ExceptionPredicate(exceptionPredicate));
    return this;
}

/**
 * 构建一个重试器时,指定当结果满足给定的谓词时进行重试
 * 
 * @param resultPredicate 不可为空,用于判断结果的谓词
 * @return RetryerBuilder<V> 返回当前的重试器构建器实例,用于链式调用
 */
public RetryerBuilder<V> retryIfResult(@Nonnull Predicate<V> resultPredicate) {
    Preconditions.checkNotNull(resultPredicate, "resultPredicate may not be null");
    this.rejectionPredicate = Predicates.or(this.rejectionPredicate, new ResultPredicate(resultPredicate));
    return this;
}
如何使用
依赖引入:
<dependency>
    <groupId>com.github.rholder</groupId>
    <artifactId>guava-retrying</artifactId>
    <version>2.0.0</version>
</dependency>
创建重试器
    public String sendMessage() throws ExecutionException {
        count = 0;
        Retryer<String> retryer = RetryerBuilder.<String>newBuilder()
                .retryIfResult(this::isRetryNeeded) //当返回结果为true时重试
                .retryIfException() //当抛出异常时重试
                .withWaitStrategy(WaitStrategies.fixedWait(10, TimeUnit.SECONDS))  //每次重试间隔10秒
                .withStopStrategy(StopStrategies.stopAfterAttempt(3)) // 最多重试3次
                .build();
        String call = null;
        try {
            call = retryer.call(() -> sendMessageInternal());
            return call;
        } catch (RetryException e) {
            // 处理重试失败的情况
            Attempt<?> attempt = e.getLastFailedAttempt();
            log.error("重试三次,发送请求失败{}",attempt.get());
            return attempt.get().toString();
        }

    }
    private String sendMessageInternal() {
        log.info("发送请求....");
        count++;
        MultiValueMap<String, Object> multiValueMap = new LinkedMultiValueMap<>();
        multiValueMap.add("operator", "1");
        if(count==3){
            HttpHeaders header = new HttpHeaders();
            header.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
            header.add("abcd","285938c60618d086d4c086adfcd9b8b9");
            HttpEntity entity = new HttpEntity<>(multiValueMap, header);
            return restTemplate.postForEntity(mainUrl, entity, String.class).getBody();
        }
        return restTemplate.postForEntity(mainUrl, multiValueMap, String.class).getBody();
    }

    private boolean isRetryNeeded(String response) {
        // 根据返回的状态码判断是否需要重试
        JSONObject jsonObject = JSONObject.parseObject(response);
        return jsonObject.getInteger("code")!=0;
    }
测试验证:
2024-11-07 14:51:13.445  INFO 18480 --- [           main] org.example.retry.CommunicationService   : 发送请求....
2024-11-07 14:51:23.621  INFO 18480 --- [           main] org.example.retry.CommunicationService   : 发送请求....
2024-11-07 14:51:33.643  INFO 18480 --- [           main] org.example.retry.CommunicationService   : 发送请求....
2024-11-07 14:51:33.655 ERROR 18480 --- [           main] org.example.retry.CommunicationService   : 重试三次,发送请求失败{"msg":"账号已在别处登录,请重新登录","code":401}
高级用法
除了固定的等待时间外,guava-retrying还支持多种复杂的等待策略,如指数退避、随机等待等。例如,使用指数退避策略可以减少短时间内频繁重试带来的压力:
.withWaitStrategy(WaitStrategies.exponentialWait(100, 1000, TimeUnit.MILLISECONDS))
通过添加RetryListener,可以监控每次重试的状态,并根据需要执行额外的操作,如记录日志或发送报警:
.withRetryListener(new RetryListener() {
    @Override
    public <V> void onRetry(Attempt<V> attempt) {
        if (attempt.hasException()) {
            System.out.println("重试次数: " + attempt.getAttemptNumber());
            attempt.getExceptionCause().printStackTrace();
        }
    }
})
除了固定的重试次数外,还可以根据其他条件停止重试,例如总重试时间超过某个阈值:
.withStopStrategy(StopStrategies.stopAfterDelay(10, TimeUnit.SECONDS))
总结
合理设置重试次数和间隔:过多的重试次数和过短的间隔时间可能会增加系统负担,导致更多的失败。
区分不同类型的错误:有些错误(如 404 Not Found)不需要重试,而有些错误(如 500 Internal Server Error)则需要重试。
使用幂等性操作:确保重试的操作是幂等的,即多次执行同一操作不会产生不同的结果。
记录重试日志:记录重试的日志可以帮助调试和监控系统行为。
用户评论