背景
在众多业务场景中,为了消除系统内的不稳定因素及逻辑错误,确保尽可能地达到预期结果,重试机制显得尤为重要。特别是在调用远程服务时,由于服务器响应延迟或网络问题,使得我们无法及时获得所需结果,甚至完全收不到响应。面对这种情况,实施一种高效且优雅的重试策略能够显著提高获取预期响应的概率。
重试机制不仅有助于应对短暂的技术故障,还能增强系统的稳定性和可靠性。通过合理设置重试次数、间隔时间和条件判断等参数,可以在不影响用户体验的前提下,自动处理一些非永久性错误。例如,在网络连接不稳定的情况下,适当增加重试次数并延长每次尝试之间的等待时间,往往能够有效克服瞬时性的网络波动,从而顺利完成服务调用。
正文
Spring Retry
不做介绍, 因为只支持在抛出异常时进行重试。
Guava Retry
基于Google Guava
库开发的一个轻量级重试组件。它提供了一种通用的方法来重试任意Java
代码片段,具备特定的停止、重试和异常处理能力。通过灵活的配置选项,开发者可以轻松地为各种场景定制重试策略,如HTTP
请求、数据库操作等。
核心概念
Retryer
:重试器对象,负责管理整个重试流程。
WaitStrategy
:等待策略,定义了两次重试之间的等待时间。
StopStrategy
:停止策略,决定了何时停止重试。
RetryListener
:重试监听器,用于监听每次重试的过程,可用于记录日志或发送通知。
Predicate
:断言函数,用于决定是否需要重试。
RetryerBuilder
重试器构建:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
|
public RetryerBuilder<V> retryIfException() { this.rejectionPredicate = Predicates.or(this.rejectionPredicate, new ExceptionClassPredicate(Exception.class)); return this; }
public RetryerBuilder<V> retryIfRuntimeException() { this.rejectionPredicate = Predicates.or(this.rejectionPredicate, new ExceptionClassPredicate(RuntimeException.class)); return this; }
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; }
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; }
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; }
|
如何使用
依赖引入:
1 2 3 4 5
| <dependency> <groupId>com.github.rholder</groupId> <artifactId>guava-retrying</artifactId> <version>2.0.0</version> </dependency>
|
创建重试器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| public String sendMessage() throws ExecutionException { count = 0; Retryer<String> retryer = RetryerBuilder.<String>newBuilder() .retryIfResult(this::isRetryNeeded) .retryIfException() .withWaitStrategy(WaitStrategies.fixedWait(10, TimeUnit.SECONDS)) .withStopStrategy(StopStrategies.stopAfterAttempt(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; }
|
测试验证:
1 2 3 4
| 2024-07-18 14:51:13.445 INFO 18480 --- [ main] org.example.retry.CommunicationService : 发送请求.... 2024-07-18 14:51:23.621 INFO 18480 --- [ main] org.example.retry.CommunicationService : 发送请求.... 2024-07-18 14:51:33.643 INFO 18480 --- [ main] org.example.retry.CommunicationService : 发送请求.... 2024-07-18 14:51:33.655 ERROR 18480 --- [ main] org.example.retry.CommunicationService : 重试三次,发送请求失败{"msg":"账号已在别处登录,请重新登录","code":401}
|
高级用法
除了固定的等待时间外,guava-retrying
还支持多种复杂的等待策略,如指数退避、随机等待等。例如,使用指数退避策略可以减少短时间内频繁重试带来的压力:
1
| .withWaitStrategy(WaitStrategies.exponentialWait(100, 1000, TimeUnit.MILLISECONDS))
|
通过添加RetryListener,可以监控每次重试的状态,并根据需要执行额外的操作,如记录日志或发送报警:
1 2 3 4 5 6 7 8 9
| .withRetryListener(new RetryListener() { @Override public <V> void onRetry(Attempt<V> attempt) { if (attempt.hasException()) { System.out.println("重试次数: " + attempt.getAttemptNumber()); attempt.getExceptionCause().printStackTrace(); } } })
|
除了固定的重试次数外,还可以根据其他条件停止重试,例如总重试时间超过某个阈值:
1
| .withStopStrategy(StopStrategies.stopAfterDelay(10, TimeUnit.SECONDS))
|
总结
- 合理设置重试次数和间隔:过多的重试次数和过短的间隔时间可能会增加系统负担,导致更多的失败。
- 区分不同类型的错误:有些错误(如
404 Not Found
)不需要重试,而有些错误(如 500 Internal Server Error
)则需要重试。
- 使用幂等性操作:确保重试的操作是幂等的,即多次执行同一操作不会产生不同的结果。
- 记录重试日志:记录重试的日志可以帮助调试和监控系统行为。