背景
在众多业务场景中,为了消除系统内的不稳定因素及逻辑错误,确保尽可能地达到预期结果,重试机制显得尤为重要。特别是在调用远程服务时,由于服务器响应延迟或网络问题,使得我们无法及时获得所需结果,甚至完全收不到响应。面对这种情况,实施一种高效且优雅的重试策略能够显著提高获取预期响应的概率。
重试机制不仅有助于应对短暂的技术故障,还能增强系统的稳定性和可靠性。通过合理设置重试次数、间隔时间和条件判断等参数,可以在不影响用户体验的前提下,自动处理一些非永久性错误。例如,在网络连接不稳定的情况下,适当增加重试次数并延长每次尝试之间的等待时间,往往能够有效克服瞬时性的网络波动,从而顺利完成服务调用。
正文
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)则需要重试。
使用幂等性操作:确保重试的操作是幂等的,即多次执行同一操作不会产生不同的结果。
记录重试日志:记录重试的日志可以帮助调试和监控系统行为。