通过使用 guava-retrying 实现灵活的重试机制

概述

本文将介绍 guava-retrying 是什么,有什么用,以及如何在项目中合理运用。

  1. guava-retrying 是一个线程安全的 Java 重试类库,提供了一种通用方法去处理任意需要重试的代码。
  2. 可以方便灵活地控制重试次数、重试时机、重试频率、停止时机等,并具有异常处理功能。

主要包括以下内容:

  1. guava-retrying 的关键点
  2. guava-retrying 的 maven 依赖
  3. 使用场景分析
  4. 基本使用
  5. guava-retrying 原理解析
  6. 具体的使用场景案例
  7. 在 spring boot 中的使用

guava-retrying 的关键点

  1. 将业务逻辑封装到实现了 Callable 接口的 call 方法中
  2. 支持设置当遇到什么异常的时候进行重试操作(也就是重新执行 Callable 接口的 call 方法)
  3. 支持设置当 call 方法的返回结果不符合预期的时候进行重试操作
  4. 支持设置重试的次数
  5. 支持设置每次重试后的等待时间
  6. 通过构造器模式创建重试对象 Retryer

guava-retrying 的 maven 依赖

1
2
3
4
5
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
</dependency>

使用场景分析

  1. 由于网络问题需要重试
  2. 某个任务的执行时间比较长,可以通过重试来不断检查执行结果是否完成

基本使用

定义业务逻辑

  • 业务逻辑封装在 Callable 对象中。
1
2
3
Callable<Boolean> callable = () -> {
return thirdApi.invoke(); // 业务逻辑
};

定义重试器

  • 通过构造器模式创建一个 Retryer 重试器对象
1
2
3
4
5
6
7
8
// 定义重试器
Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
.retryIfResult(Predicates.<Boolean>isNull()) // 如果结果为空则重试
.retryIfExceptionOfType(IOException.class) // 发生IO异常则重试
.retryIfRuntimeException() // 发生运行时异常则重试
.withWaitStrategy(WaitStrategies.incrementingWait(10, TimeUnit.SECONDS, 10, TimeUnit.SECONDS)) // 每次重试后的等待时间配置:首次10s, 之后每次增加 10s
.withStopStrategy(StopStrategies.stopAfterAttempt(4)) // 停止重试配置:允许执行4次(首次执行 + 最多重试3次)
.build();

通过重试器来执行代码

  • 通过重试器的 call 方法执行业务逻辑代码
1
2
3
4
5
try {
retryer.call(callable); // 执行
} catch (final Exception e) { // 重试次数超过阈值或被强制中断
log.error("出现异常", e);
}
  • 也可以通过 wrap 方法返回 RetryerCallable 对象,之后再通过 RetryerCallable 对象中的 call 执行业务逻辑
1
2
3
4
5
6
try {
final Retryer.RetryerCallable<Map<String, Object>> retryerCallable = retryer.wrap(callable);
final Map<String, Object> httpResult = retryerCallable.call();
} catch (final Exception e) { // 重试次数超过阈值或被强制中断
log.error("出现异常", e);
}

分析上述代码:

  1. 首先定义了一个 Callable 对象,其中执行我们需要重试的业务逻辑。
  2. 通过 RetryerBuilder 构造重试器,构造包含如下部分:
1
2
3
4
重试条件 retryIfResult、retryIfExceptionOfType、retryIfRuntimeException
重试等待策略(延迟)withWaitStrategy
重试停止策略 withStopStrategy
阻塞策略、超时限制、注册重试监听器(上述代码未使用)
  1. 通过 retryer.call 执行任务
  2. 当重试次数超过设定值或者被强制中断时,会抛出异常,需要捕获处理

guava-retrying 原理解析

下面是 guava-retrying 的 call 方法的解析

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
public V call(Callable<V> callable) throws ExecutionException, RetryException {
long startTime = System.nanoTime();
for (int attemptNumber = 1; ; attemptNumber++) { // 1. 通过 for 循环来控制最大的重试次数
Attempt<V> attempt;
try {
V result = attemptTimeLimiter.call(callable); // 2. 执行业务逻辑
attempt = new ResultAttempt<V>(result, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
} catch (Throwable t) { // 3. 执行过程中出现异常后重试
attempt = new ExceptionAttempt<V>(t, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
}

for (RetryListener listener : listeners) { // 4. 增加重试监听
listener.onRetry(attempt);
}

if (!rejectionPredicate.apply(attempt)) { // 5. 判断业务逻辑是否正常执行完毕,是否有异常发生
return attempt.get();
}
if (stopStrategy.shouldStop(attempt)) { // 6. 判断是否终止重试操作
throw new RetryException(attemptNumber, attempt);
} else {
long sleepTime = waitStrategy.computeSleepTime(attempt); // 7. 计算等待时间
try {
blockStrategy.block(sleepTime); // 8. 阻塞当前线程,执行等待
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RetryException(attemptNumber, attempt);
}
}
}
}

具体的使用场景案例

在下面的案例中通过 MyRetryListener 来监听重试的过程,定义如下

1
2
3
4
5
6
7
8
9
10
11
 public static class MyRetryListener implements RetryListener {

@Override
public <V> void onRetry(final Attempt<V> attempt) {
log.info(String.format("执行结果:%s, 异常:%s, 当前重试次数:%s, 当前累计执行时长:%s 毫秒",
attempt.hasResult() ? attempt.getResult() : null,
attempt.hasException() ? attempt.getExceptionCause().getClass().getName() : null,
attempt.getAttemptNumber(),
attempt.getDelaySinceFirstAttempt()));
}
}

执行的结果为空的情况下重试

  1. 测试代码如下
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
@Test
public void test_retry_getResult() {
// 定义执行任务
final Callable<Map<String, Object>> callable = () -> {
try {
// 暂停 3 s, 模拟数据库查询或者其他耗时的任务
TimeUnit.SECONDS.sleep(3);
// 设置响应结果为 null 的情况
return null;
} catch (final Exception e) {
throw new MyException("执行任务出现异常", e);
}

};

// 定义重试器
final Retryer<Map<String, Object>> retryer = RetryerBuilder.<Map<String, Object>>newBuilder()
.retryIfResult(Predicates.isNull()) // 如果结果为空则重试
.retryIfExceptionOfType(MyException.class) // 发生指定类型的异常则重试
.withRetryListener(new MyRetryListener()) // 出现异常的情况下回调处理
.withWaitStrategy(WaitStrategies
.incrementingWait(10, TimeUnit.SECONDS, 10, TimeUnit.SECONDS)) // 每次重试后的等待时间配置:首次10s, 之后每次增加 10s
.withStopStrategy(StopStrategies.stopAfterAttempt(4)) // 停止重试配置:允许执行4次(首次执行 + 最多重试3次)
.build();

// 用重试器执行任务
try {
final Map<String, Object> result = retryer.call(callable);
System.out.println(String.format("结果:%s", JsonUtils.toJSONString(result, true)));
Assert.assertTrue(Objects.nonNull(result));
} catch (final Exception e) { // 重试次数超过阈值或被强制中断
log.error("出现异常", e);
}
}
  1. 通过监听发现重试的原因是:结果为空,具体如下
1
2
3
4
5
6
13:09:30.736 [main] INFO com.ckjava.retrying.TestRetrying - 执行结果:null, 异常:null, 当前重试次数:1, 当前累计执行时长:3003 毫秒
13:09:43.755 [main] INFO com.ckjava.retrying.TestRetrying - 执行结果:null, 异常:null, 当前重试次数:2, 当前累计执行时长:16026 毫秒
13:10:06.766 [main] INFO com.ckjava.retrying.TestRetrying - 执行结果:null, 异常:null, 当前重试次数:3, 当前累计执行时长:39037 毫秒
13:10:39.796 [main] INFO com.ckjava.retrying.TestRetrying - 执行结果:null, 异常:null, 当前重试次数:4, 当前累计执行时长:72067 毫秒
13:10:39.801 [main] ERROR com.ckjava.retrying.TestRetrying - 出现异常
com.github.rholder.retry.RetryException: Retrying failed to complete successfully after 4 attempts.

出现异常的情况下重试

  1. 测试代码如下
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
@Test
public void test_retry_encounterException() {
// 定义执行任务
final Callable<Map<String, Object>> callable = () -> {
// 暂停 3 s, 模拟请求其他应用的 api 或者容易出现异常的操作
TimeUnit.SECONDS.sleep(3);
// 设置出现异常的情况
throw new MyException("执行任务出现异常");
};

// 定义重试器
final Retryer<Map<String, Object>> retryer = RetryerBuilder.<Map<String, Object>>newBuilder()
.retryIfResult(Predicates.isNull()) // 如果结果为空则重试
.retryIfExceptionOfType(MyException.class) // 发生指定类型的异常则重试
.withRetryListener(new MyRetryListener()) // 出现异常的情况下回调处理
.withWaitStrategy(WaitStrategies
.incrementingWait(10, TimeUnit.SECONDS, 10, TimeUnit.SECONDS)) // 每次重试后的等待时间配置:首次10s, 之后每次增加 10s
.withStopStrategy(StopStrategies.stopAfterAttempt(4)) // 停止重试配置:允许执行4次(首次执行 + 最多重试3次)
.build();

// 用重试器执行任务
try {
final Map<String, Object> result = retryer.call(callable);
System.out.println(String.format("结果:%s", JsonUtils.toJSONString(result, true)));
Assert.assertTrue(Objects.nonNull(result));
} catch (final Exception e) { // 重试次数超过阈值或被强制中断
log.error("出现异常", e);
}
}
  1. 通过监听发现重试的原因是:出现了异常,具体如下
1
2
3
4
5
6
13:11:31.581 [main] INFO com.ckjava.retrying.TestRetrying - 执行结果:null, 异常:com.ckjava.retrying.TestRetrying$MyException, 当前重试次数:1, 当前累计执行时长:3009 毫秒
13:11:44.596 [main] INFO com.ckjava.retrying.TestRetrying - 执行结果:null, 异常:com.ckjava.retrying.TestRetrying$MyException, 当前重试次数:2, 当前累计执行时长:16025 毫秒
13:12:07.612 [main] INFO com.ckjava.retrying.TestRetrying - 执行结果:null, 异常:com.ckjava.retrying.TestRetrying$MyException, 当前重试次数:3, 当前累计执行时长:39041 毫秒
13:12:40.625 [main] INFO com.ckjava.retrying.TestRetrying - 执行结果:null, 异常:com.ckjava.retrying.TestRetrying$MyException, 当前重试次数:4, 当前累计执行时长:72054 毫秒
13:12:40.628 [main] ERROR com.ckjava.retrying.TestRetrying - 出现异常
com.github.rholder.retry.RetryException: Retrying failed to complete successfully after 4 attempts.

在 SpringBoot 中的使用

这里通过调用其他应用的 api 来举例说明具体的使用。

通过@Configuration配置类定义重试器

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
package com.toulezu.test.config;

import com.github.rholder.retry.Attempt;
import com.github.rholder.retry.RetryListener;
import com.github.rholder.retry.Retryer;
import com.github.rholder.retry.RetryerBuilder;
import com.github.rholder.retry.StopStrategies;
import com.github.rholder.retry.WaitStrategies;
import com.google.common.base.Predicates;
import com.toulezu.test.exception.MyException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Map;
import java.util.concurrent.TimeUnit;

@Slf4j
@Configuration
public class RetryConfig {

@Bean
public Retryer<Map<String, Object>> jiaRiRetry() {
return RetryerBuilder.<Map<String, Object>>newBuilder()
.retryIfResult(Predicates.isNull()) // 如果结果为空则重试
.retryIfExceptionOfType(MyException.class) // 发生指定类型的异常则重试
.withRetryListener(new MyRetryListener()) // 出现异常的情况下回调处理
.withWaitStrategy(WaitStrategies
.incrementingWait(10, TimeUnit.SECONDS, 10, TimeUnit.SECONDS)) // 每次重试后的等待时间配置:首次10s, 之后每次增加 10s
.withStopStrategy(StopStrategies.stopAfterAttempt(4)) // 停止重试配置:允许执行4次(首次执行 + 最多重试3次)
.build();
}

/**
* 重试监听处理
*/
public static class MyRetryListener implements RetryListener {

@Override
public <V> void onRetry(final Attempt<V> attempt) {
log.info(String.format("执行结果:%s, 异常:%s, 当前重试次数:%s, 当前累计执行时长:%s 毫秒",
attempt.hasResult() ? attempt.getResult() : null,
attempt.hasException() ? attempt.getExceptionCause().getClass().getName() : null,
attempt.getAttemptNumber(),
attempt.getDelaySinceFirstAttempt()));
}
}
}

定义业务逻辑

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
package com.toulezu.test.function;

import com.ckjava.xutils.HttpClientUtils;
import com.ckjava.xutils.JsonUtils;
import com.ckjava.xutils.http.HttpResult;
import com.fasterxml.jackson.core.type.TypeReference;
import com.toulezu.test.exception.MyException;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Callable;

/**
* @author ckjava
*
*/
public class JiaRiCallable implements Callable<Map<String, Object>> {

private String year;

public JiaRiCallable(final String year) {
this.year = year;
}

@Override
public Map<String, Object> call() throws Exception {
final Map<String, String> headers = new HashMap<>(1);
headers.put("Content-Type", "application/json; charset=utf-8");
final Map<String, String> params = new HashMap<>(1);
params.put("d", year);
final HttpResult httpResult =
HttpClientUtils.get(HttpClientUtils.HttpRequestInfo.builder().url("http://tool.bitefu.net/jiari").headers(headers).parameters(params)
.build());
try {
final Map<String, Object> result = JsonUtils.readJavaObject(httpResult.getResponseBody(), new TypeReference<LinkedHashMap<String, Object>>() {
}).orElse(null);
return result;
} catch (final Exception e) {
throw new MyException("解析响应报文出现异常", e);
}
}
}

通过重试器执行业务逻辑

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
package com.toulezu.test.web;

import com.ckjava.xutils.http.HttpResponse;
import com.github.rholder.retry.Retryer;
import com.toulezu.test.function.JiaRiCallable;
import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.Map;

@Api
@RestController
public class JiaRiController {

@Resource
private Retryer<Map<String, Object>> jiaRiRetry;

@GetMapping(value = "/jiaRi")
public HttpResponse<Map<String, Object>> getJiaRi(
@RequestParam String year) {
try {
return HttpResponse.getReturn(jiaRiRetry.call(new JiaRiCallable(year)), 200, null);
} catch (Exception e) {
return HttpResponse.getReturn(e);
}
}
}

参考

打赏