乐趣区

关于java:使用-Resilience4j-框架实现重试机制


在本文中,咱们将从疾速介绍 Resilience4j 开始,而后深入探讨其 Retry 模块。咱们将理解何时、如何应用它,以及它提供的性能。在此过程中,咱们还将学习实现重试时的一些良好实际。

代码示例

本文在 GitHu 上附有工作代码示例。

什么是 Resilience4j?

当应用程序通过网络进行通信时,会有很多出错的状况。因为连贯断开、网络故障、上游服务不可用等,操作可能会超时或失败。应用程序可能会互相过载、无响应甚至解体。

Resilience4j 是一个 Java 库,能够帮忙咱们构建弹性和容错的应用程序。它提供了一个框架,可编写代码以避免和解决此类问题

Resilience4j 为 Java 8 及更高版本编写,实用于函数接口、lambda 表达式和办法援用等构造。

Resilience4j 模块

让咱们疾速浏览一下这些模块及其用处:

模块 目标
Retry 主动重试失败的近程操作
RateLimiter 限度咱们在肯定工夫内调用近程操作的次数
TimeLimiter 调用近程操作时设置工夫限度
Circuit Breaker 当近程操作继续失败时,疾速失败或执行默认操作
Bulkhead 限度并发近程操作的数量
Cache 存储低廉的近程操作的后果

应用范式

尽管每个模块都有其形象,但通常的应用范式如下:

  1. 创立一个 Resilience4j 配置对象
  2. 为此类配置创立一个 Registry 对象
  3. 从注册表创立或获取 Resilience4j 对象
  4. 将近程操作编码为 lambda 表达式或函数式接口或通常的 Java 办法
  5. 应用提供的辅助办法之一围绕第 4 步中的代码创立装璜器或包装器
  6. 调用装璜器办法来调用近程操作
    步骤 1-5 通常在应用程序启动时实现一次。让咱们看看重试模块的这些步骤:
RetryConfig config = RetryConfig.ofDefaults(); // ----> 1
RetryRegistry registry = RetryRegistry.of(config); // ----> 2
Retry retry = registry.retry("flightSearchService", config); // ----> 3


FlightSearchService searchService = new FlightSearchService();
SearchRequest request = new SearchRequest("NYC", "LAX", "07/21/2020");
Supplier<List<Flight>> flightSearchSupplier =
  () -> searchService.searchFlights(request); // ----> 4


Supplier<List<Flight>> retryingFlightSearch =
  Retry.decorateSupplier(retry, flightSearchSupplier); // ----> 5


System.out.println(retryingFlightSearch.get()); // ----> 6

什么时候应用重试?

近程操作能够是通过网络收回的任何申请。通常,它是以下之一:

  1. 向 REST 端点发送 HTTP 申请
  2. 调用近程过程 (RPC) 或 Web 服务
  3. 从数据存储(SQL/NoSQL 数据库、对象存储等)读取和写入数据
  4. 向音讯代理(RabbitMQ/ActiveMQ/Kafka 等)发送和接管音讯

当近程操作失败时,咱们有两种抉择——立刻向咱们的客户端返回谬误,或者重试操作。如果重试胜利,这对客户来说是件坏事——他们甚至不用晓得这是一个长期问题。

抉择哪个选项取决于谬误类型(刹时或永恒)、操作(幂等或非幂等)、客户端(人或应用程序)和用例。

暂时性谬误是临时的,通常,如果重试,操作很可能会胜利。申请被上游服务限度、连贯断开或因为某些服务临时不可用而超时就是例子。

来自 REST API 的硬件故障或 404(未找到)响应是永久性谬误的示例,重试杯水车薪

如果咱们想利用重试,操作必须是幂等的。假如近程服务接管并解决了咱们的申请,但在发送响应时呈现问题。在这种状况下,当咱们重试时,咱们不心愿服务将申请视为新申请或返回意外谬误(想想银行转账)。

重试会减少 API 的响应工夫。如果客户端是另一个应用程序,如 cron 作业或守护过程,这可能不是问题。然而,如果是一个人,有时最好做出响应,疾速失败并提供反馈,而不是在咱们一直重试时让这个人期待。

对于某些要害用例,可靠性可能比响应工夫更重要,即便客户是集体,咱们也可能须要实现重试。银行转账或旅行社预订航班和旅行酒店的转账就是很好的例子 – 用户冀望可靠性,而不是对此类用例的即时响应。咱们能够通过立刻告诉用户咱们已承受他们的申请并在实现后告诉他们来做出响应。

应用 Resilience4j 重试模块

RetryRegistryRetryConfigRetry 是 resilience4j-retry 中的次要形象。RetryRegistry 是用于创立和治理 Retry 对象的工厂。RetryConfig 封装了诸如应该尝试重试多少次、尝试之间期待多长时间等配置。每个 Retry 对象都与一个 RetryConfig 相关联。Retry 提供了辅助办法来为蕴含近程调用的函数式接口或 lambda 表达式创立装璜器。

让咱们看看如何应用 retry 模块中可用的各种性能。假如咱们正在为一家航空公司建设一个网站,以容许其客户搜寻和预订航班。咱们的服务与 FlightSearchService 类封装的近程服务通信。

简略重试

在简略重试中,如果在近程调用期间抛出 RuntimeException,则重试该操作。咱们能够配置尝试次数、尝试之间期待多长时间等:

RetryConfig config = RetryConfig.custom()
  .maxAttempts(3)
  .waitDuration(Duration.of(2, SECONDS))
  .build();


// Registry, Retry creation omitted


FlightSearchService service = new FlightSearchService();
SearchRequest request = new SearchRequest("NYC", "LAX", "07/31/2020");
Supplier<List<Flight>> flightSearchSupplier =
  () -> service.searchFlights(request);


Supplier<List<Flight>> retryingFlightSearch =
  Retry.decorateSupplier(retry, flightSearchSupplier);


System.out.println(retryingFlightSearch.get());

咱们创立了一个 RetryConfig,指定咱们最多要重试 3 次,并在两次尝试之间期待 2 秒。如果咱们改用 RetryConfig.ofDefaults() 办法,则将应用 3 次尝试和 500 毫秒期待持续时间的默认值。

咱们将航班搜寻调用示意为 lambda 表达式 – List<Flight>SupplierRetry.decorateSupplier() 办法应用重试性能装璜此 Supplier。最初,咱们在装璜过的 Supplier 上调用 get() 办法来进行近程调用。

如果咱们想创立一个装璜器并在代码库的不同地位重用它,咱们将应用 decorateSupplier()。如果咱们想创立它并立刻执行它,咱们能够应用 executeSupplier() 实例办法代替:

List<Flight> flights = retry.executeSupplier(() -> service.searchFlights(request));
这是显示第一个申请失败而后第二次尝试胜利的示例输入:Searching for flights; current time = 20:51:34 975
Operation failed
Searching for flights; current time = 20:51:36 985
Flight search successful
[Flight{flightNumber='XY 765', flightDate='07/31/2020', from='NYC', to='LAX'}, ...]

在已检异样上重试

当初,假如咱们要重试已检查和未查看的异样。假如咱们正在调用
FlightSearchService.searchFlightsThrowingException(),它能够抛出一个已查看的 Exception。因为 Supplier 不能抛出已查看的异样,咱们会在这一行失去编译器谬误:

Supplier<List<Flight>> flightSearchSupplier =
  () -> service.searchFlightsThrowingException(request);

咱们可能会尝试在 lambda 表达式中解决 Exception 并返回 Collections.emptyList(),但这看起来不太好。更重要的是,因为咱们本人捕捉 Exception,重试不再起作用:

ExceptionSupplier<List<Flight>> flightSearchSupplier = () -> {
    try {return service.searchFlightsThrowingException(request);
    } catch (Exception e) {// don't do this, this breaks the retry!}
    return Collections.emptyList();};

那么当咱们想要重试近程调用可能抛出的所有异样时,咱们应该怎么做呢?咱们能够应用
Retry.decorateCheckedSupplier()(或 executeCheckedSupplier() 实例办法)代替 Retry.decorateSupplier()

CheckedFunction0<List<Flight>> retryingFlightSearch =
  Retry.decorateCheckedSupplier(retry,
    () -> service.searchFlightsThrowingException(request));


try {System.out.println(retryingFlightSearch.apply());
} catch (...) {// handle exception that can occur after retries are exhausted}

Retry.decorateCheckedSupplier() 返回一个 CheckedFunction0,它示意一个没有参数的函数。请留神对 CheckedFunction0 对象的 apply() 调用以调用近程操作。

如果咱们不想应用 SuppliersRetry 提供了更多的辅助装璜器办法,如 decorateFunction()decorateCheckedFunction()decorateRunnable()decorateCallable() 等,以与其余语言构造一起应用。decorate*decorateChecked* 版本之间的区别在于,decorate* 版本在 RuntimeExceptions 上重试,而 decorateChecked* 版本在 Exception 上重试。

有条件重试

下面的简略重试示例展现了如何在调用近程服务时遇到 RuntimeException 或已查看 Exception 时重试。在理论利用中,咱们可能不想对所有异样都重试。例如,如果咱们失去一个
AuthenticationFailedException 重试雷同的申请将杯水车薪。当咱们进行 HTTP 调用时,咱们可能想要查看 HTTP 响应状态代码或在响应中查找特定的应用程序错误代码来决定是否应该重试。让咱们看看如何实现这种有条件的重试。

Predicate-based 条件重试

假如航空公司的航班服务定期初始化其数据库中的航班数据。对于给定日期的飞行数据,此外部操作须要几秒钟工夫。如果咱们在初始化过程中调用当天的航班搜寻,该服务将返回一个特定的错误代码 FS-167。航班搜寻文档说这是一个长期谬误,能够在几秒钟后重试该操作。

让咱们看看如何创立 RetryConfig

RetryConfig config = RetryConfig.<SearchResponse>custom()
  .maxAttempts(3)
  .waitDuration(Duration.of(3, SECONDS))
  .retryOnResult(searchResponse -> searchResponse
    .getErrorCode()
    .equals("FS-167"))
  .build();

咱们应用 retryOnResult() 办法并传递执行此查看的 Predicate。这个 Predicate 中的逻辑能够像咱们想要的那样简单——它能够是对一组错误代码的查看,也能够是一些自定义逻辑来决定是否应该重试搜寻。

Exception-based 条件重试

假如咱们有一个通用异样
FlightServiceBaseException,当在与航空公司的航班服务交互期间产生任何意外时会抛出该异样。作为个别策略,咱们心愿在抛出此异样时重试。然而咱们不想重试 SeatsUnavailableException 的一个子类 – 如果航班上没有可用座位,重试将杯水车薪。咱们能够通过像这样创立 RetryConfig 来做到这一点:

RetryConfig config = RetryConfig.custom()
  .maxAttempts(3)
  .waitDuration(Duration.of(3, SECONDS))
  .retryExceptions(FlightServiceBaseException.class)
  .ignoreExceptions(SeatsUnavailableException.class)
  .build();

retryExceptions() 中,咱们指定了一个异样列表。ignoreExceptions() 将重试与此列表中的异样匹配或继承的任何异样。咱们把咱们想疏忽而不是重试的那些放入ignoreExceptions()。如果代码在运行时抛出一些其余异样,比方 IOException,它也不会被重试。

假如即便对于给定的异样,咱们也不心愿在所有状况下都重试。兴许咱们只想在异样具备特定错误代码或异样音讯中的特定文本时重试。在这种状况下,咱们能够应用 retryOnException 办法:

Predicate<Throwable> rateLimitPredicate = rle ->
  (rle instanceof  RateLimitExceededException) &&
  "RL-101".equals(((RateLimitExceededException) rle).getErrorCode());


RetryConfig config = RetryConfig.custom()
  .maxAttempts(3)
  .waitDuration(Duration.of(1, SECONDS))
  .retryOnException(rateLimitPredicate)
  build();

与 predicate-based(基于谓词)的条件重试一样,谓词内的查看能够依据须要复杂化。

退却策略

到目前为止,咱们的示例有固定的重试等待时间。通常咱们心愿在每次尝试后减少等待时间——这是为了让近程服务有足够的工夫在以后过载的状况下进行复原。咱们能够应用 IntervalFunction 来做到这一点。

IntervalFunction 是一个函数式接口——它是一个以尝试次数为参数并以毫秒为单位返回等待时间的 Function

随机距离

这里咱们指定尝试之间的随机等待时间:

RetryConfig config = RetryConfig.custom()
.maxAttempts(4)
.intervalFunction(IntervalFunction.ofRandomized(2000))
.build();

IntervalFunction.ofRandomized() 有一个关联的 randomizationFactor。咱们能够将其设置为 ofRandomized() 的第二个参数。如果未设置,则采纳默认值 0.5。这个 randomizationFactor 决定了随机值的散布范畴。因而,对于下面的默认值 0.5,生成的等待时间将介于 1000 毫秒(2000 – 2000 0.5)和 3000 毫秒(2000 + 2000 0.5)之间。

这种行为的示例输入如下:

Searching for flights; current time = 20:27:08 729
Operation failed
Searching for flights; current time = 20:27:10 643
Operation failed
Searching for flights; current time = 20:27:13 204
Operation failed
Searching for flights; current time = 20:27:15 236
Flight search successful
[Flight{flightNumber='XY 765', flightDate='07/31/2020', from='NYC', to='LAX'},...]

指数距离

对于指数退却,咱们指定两个值 – 初始等待时间和乘数。在这种办法中,因为乘数,等待时间在尝试之间呈指数增长。例如,如果咱们指定初始等待时间为 1 秒,乘数为 2,则重试将在 1 秒、2 秒、4 秒、8 秒、16 秒等之后进行。当客户端是后台作业或守护过程时,此办法是举荐的办法。

以下是咱们如何为指数退却创立 RetryConfig

RetryConfig config = RetryConfig.custom()
.maxAttempts(6)
.intervalFunction(IntervalFunction.ofExponentialBackoff(1000, 2))
.build();

这种行为的示例输入如下:

Searching for flights; current 
time = 20:37:02 684

Operation failed

Searching for flights; current time = 20:37:03 727

Operation failed

Searching for flights; current time = 20:37:05 731

Operation failed

Searching for flights; current time = 20:37:09 731

Operation failed

Searching for flights; current time = 20:37:17 731

IntervalFunction 还提供了一个 exponentialRandomBackoff() 办法,它联合了上述两种办法。咱们还能够提供 IntervalFunction 的自定义实现。

重试异步操作

直到现在咱们看到的例子都是同步调用。让咱们看看如何重试异步操作。假如咱们像这样异步搜寻航班:

CompletableFuture.supplyAsync(() -> service.searchFlights(request))
  .thenAccept(System.out::println);

searchFlight() 调用产生在不同的线程上,当它返回时,返回的 List<Flight> 被传递给 thenAccept(),它只是打印它。

咱们能够应用 Retry 对象上的 executeCompletionStage() 办法对上述异步操作进行重试。此办法采纳两个参数 – 一个 ScheduledExecutorService 将在其上安顿重试,以及一个 Supplier<CompletionStage> 将被装璜。它装璜并执行 CompletionStage,而后返回一个 CompletionStage,咱们能够像以前一样调用 thenAccept

ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();


Supplier<CompletionStage<List<Flight>>> completionStageSupplier =
  () -> CompletableFuture.supplyAsync(() -> service.searchFlights(request));


retry.executeCompletionStage(scheduler, completionStageSupplier)
.thenAccept(System.out::println);

在理论应用程序中,咱们将应用共享线程池 (
Executors.newScheduledThreadPool()) 来调度重试,而不是此处显示的单线程调度执行器。

重试事件

在所有这些例子中,装璜器都是一个黑盒子——咱们不晓得什么时候尝试失败了,框架代码正在尝试重试。假如对于给定的申请,咱们想要记录一些详细信息,例如尝试计数或下一次尝试之前的等待时间。咱们能够应用在不同执行点公布的重试事件来做到这一点。Retry 有一个 EventPublisher,它具备 onRetry()onSuccess() 等办法。

咱们能够通过实现这些监听器办法来收集和记录详细信息:

Retry.EventPublisher publisher = retry.getEventPublisher();

publisher.onRetry(event -> System.out.println(event.toString()));

publisher.onSuccess(event -> System.out.println(event.toString()));

相似地,RetryRegistry 也有一个 EventPublisher,它在 Retry 对象被增加或从注册表中删除时公布事件。

重试指标

Retry 保护计数器以跟踪操作的次数

  1. 第一次尝试胜利
  2. 重试后胜利
  3. 没有重试就失败了
  4. 重试后仍失败

每次执行装璜器时,它都会更新这些计数器。

为什么要捕捉指标?

捕捉并定期剖析指标能够让咱们深刻理解上游服务的行为。它还能够帮忙辨认瓶颈和其余潜在问题

例如,如果咱们发现某个操作通常在第一次尝试时失败,咱们能够考察其起因。如果咱们发现咱们的申请在建设连贯时受到限制或超时,则可能表明近程服务须要额定的资源或容量。

如何捕捉指标?

Resilience4j 应用 Micrometer 公布指标。Micrometer 为监控零碎(如 Prometheus、Azure Monitor、New Relic 等)提供了仪表客户端的外观。因而咱们能够将指标公布到这些零碎中的任何一个或在它们之间切换,而无需更改咱们的代码。

首先,咱们像平常一样创立 RetryConfigRetryRegistryRetry。而后,咱们创立一个 MeterRegistry 并将 etryRegistry 绑定到它:

MeterRegistry meterRegistry = new SimpleMeterRegistry();

TaggedRetryMetrics.ofRetryRegistry(retryRegistry).bindTo(meterRegistry);

运行几次可重试操作后,咱们显示捕捉的指标:

Consumer<Meter> meterConsumer = meter -> {String desc = meter.getId().getDescription();
  String metricName = meter.getId().getTag("kind");
  Double metricValue = StreamSupport.stream(meter.measure().spliterator(), false)
    .filter(m -> m.getStatistic().name().equals("COUNT"))
    .findFirst()
    .map(m -> m.getValue())
    .orElse(0.0);
  System.out.println(desc + "-" + metricName + ":" + metricValue);
};
meterRegistry.forEachMeter(meterConsumer);

一些示例输入如下:

The number of successful calls without a retry attempt - successful_without_retry: 4.0

The number of failed calls without a retry attempt - failed_without_retry: 0.0

The number of failed calls after a retry attempt - failed_with_retry: 0.0

The number of successful calls after a retry attempt - successful_with_retry: 6.0

当然,在理论利用中,咱们会将数据导出到监控零碎并在仪表板上查看。

重试时的注意事项和良好实际

服务通常提供具备内置重试机制的客户端库或 SDK。对于云服务尤其如此。例如,Azure CosmosDB 和 Azure 服务总线为客户端库提供内置重试工具。它们容许应用程序设置重试策略来管制重试行为。

在这种状况下,最好应用内置的重试而不是咱们本人的编码。如果咱们的确须要本人编写,咱们应该禁用内置的默认重试策略 – 否则,它可能导致嵌套重试,其中应用程序的每次尝试都会导致客户端库的屡次尝试

一些云服务记录刹时错误代码。例如,Azure SQL 提供了它冀望数据库客户端重试的错误代码列表。在决定为特定操作增加重试之前,最好检查一下服务提供商是否有这样的列表。

另一个好的做法是 将咱们在 RetryConfig 中应用的值(例如最大尝试次数、等待时间和可重试错误代码和异样)作为咱们服务之外的配置进行保护。如果咱们发现新的暂时性谬误或者咱们须要调整尝试之间的距离,咱们能够在不构建和重新部署服务的状况下进行更改。

通常在重试时,框架代码中的某处可能会产生 Thread.sleep()。对于在重试之间有等待时间的同步重试就是这种状况。如果咱们的代码在 Web 应用程序的上下文中运行,则 Thread 很可能是 Web 服务器的申请解决线程。因而,如果咱们进行过多的重试,则会升高应用程序的吞吐量

论断

在本文中,咱们理解了 Resilience4j 是什么,以及如何应用它的重试模块使咱们的应用程序能够在应答长期谬误具备弹性。咱们钻研了配置重试的不同办法,以及在不同办法之间做出决定的一些示例。咱们学习了一些在施行重试时要遵循的良好实际,以及收集和剖析重试指标的重要性。

您能够应用 GitHub 上的代码尝试一个残缺的应用程序来演示这些想法。


本文译自:Implementing Retry with Resilience4j – Reflectoring

退出移动版