当前位置: 首页 > 后端技术 > Java

Java项目中使用Resilience4j框架的异步超时处理

时间:2023-04-01 22:37:39 Java

本系列到此为止,我们学习了Resilience4j及其[Retry](https://icodewalker.com/blog/...)和[RateLimiter](https//icodewalker.com/blog/…)模块。在本文中,我们将继续使用TimeLimiter探索Resilience4j。我们将看到它解决了什么问题,何时以及如何使用它,并查看一些示例。代码示例本文附有工作代码示例[在GitHub上](https://github.com/thombergs/...)。什么是Resilience4j?请参阅上一篇文章中的描述,快速了解[Resilience4j的一般工作原理](https://icodewalker.com/blog/…)。什么是时间限制?设置我们愿意等待操作完成的时间限制称为时间限制。如果操作没有在我们指定的时间内完成,我们希望通过超时错误得到通知。有时这也称为“设定截止日期”。我们这样做的主要原因之一是确保我们不会让用户或客户无限期地等待。不提供任何反馈的缓慢服务会使用户感到沮丧。我们对操作设置时间限制的另一个原因是确保我们不会无限期地占用服务器资源。我们在使用Spring的@Transactional注解时指定的超时值就是一个例子——在这种情况下,我们不想长时间占用数据库资源。何时使用Resilience4jTimeLimiter?Resilience4j的TimeLimiter可用于为使用CompleteableFutures实现的异步操作设置时间限制(超时)。Java8中引入的CompletableFuture类使异步、非阻塞编程变得更加容易。慢速方法可以在不同的线程上执行,释放当前线程来处理其他任务。我们可以提供一个当slowMethod()返回时执行的回调:intslowMethod(){//耗时计算或远程操作return42;}CompletableFuture.supplyAsync(this::slowMethod).thenAccept(System.out::println);这里的slowMethod()可以是一些计算或远程操作。通常,我们希望在进行此类异步调用时设置时间限制。我们不想无限期地等待slowMethod()返回。例如,如果slowMethod()花费的时间超过一秒,我们可能想要返回一个先前计算的、缓存的值,甚至可能会得到一个错误。在Java8的CompletableFuture中,没有简单的方法来设置异步操作的时间限制。CompletableFuture实现了Future接口,Future有一个重载的get()方法来指定我们可以等待多长时间:CompletableFuturecompletableFuture=CompletableFuture.supplyAsync(this::slowMethod);Integerresult=completableFuture.get(3000,TimeUnit.毫秒);System.out.println(结果);但是这里有一个问题——get()方法是一个阻塞调用。所以它首先违背了使用CompletableFuture的目的,即释放当前线程。这就是Resilience4j的TimeLimiter解决的问题——它允许我们对异步操作设置时间限制,同时保留在Java8中使用CompletableFuture的非阻塞优势。CompletableFuture的这个限制在Java9中已经解决。我们可以直接使用设置时间限制Java9及更高版本中CompletableFuture上的orTimeout()或completeOnTimeout()等方法。但是,通过Resilience4J的指标和事件,与普通Java9解决方案相比,它仍然提供附加值。Resilience4jTimeLimiter概念TimeLimiter支持Future和CompletableFuture。但与Future一起使用相当于Future.get(longtimeout,TimeUnitunit)。因此,在本文的其余部分,我们将重点关注CompletableFuture。与其他Resilience4j模块一样,TimeLimiter通过使用所需功能装饰我们的代码来工作-在这种情况下,如果操作未在指定的timeoutDuration内完成,则返回TimeoutException。我们为TimeLimiter提供timeoutDuration、ScheduledExecutorService和异步操作本身,表示为CompletionStage的Supplier。它返回一个CompletionStage装饰供应商。在内部,它使用调度程序来安排超时任务——通过抛出TimeoutException来完成CompletableFuture的任务。如果操作先完成,TimeLimiter会取消内部超时任务。除了timeoutDuration,还有一个与TimeLimiter关联的配置cancelRunningFuture。此配置仅适用于Futures而不适用于CompletableFutures。当超时发生时,它会在抛出TimeoutException之前取消正在运行的Future。使用Resilience4jTimeLimiter模块TimeLimiterRegistry、TimeLimiterConfig和TimeLimiter是resilience4j-timelimiter的主要抽象。TimeLimiterRegistry是一个用于创建和管理TimeLimiter对象的工厂。TimeLimiterConfig封装了timeoutDuration和cancelRunningFuture配置。每个TimeLimiter对象都与一个TimeLimiterConfig相关联。TimeLimiter提供辅助方法来为Future和CompletableFutureSuppliers创建或执行装饰器。让我们看看如何使用TimeLimiter模块中可用的各种函数。我们将使用与本系列前面几篇文章相同的示例。假设我们正在为一家航空公司建立一个网站,以允许其客户搜索和预订航班。我们的服务与FlightSearchService类封装的远程服务对话。第一步是创建一个TimeLimiterConfig:TimeLimiterConfigconfig=TimeLimiterConfig.ofDefaults();这将创建一个TimeLimiterConfig,其默认值为timeoutDuration(1000ms)和cancelRunningFuture(true)。假设我们想将超时值设置为2s而不是默认值:TimeLimiterConfigconfig=TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(2)).build();然后我们创建一个TimeLimiter:TimeLimiterRegistryregistry=TimeLimiterRegistry.of(config);TimeLimiterlimiter=registry.timeLimiter("flightSearch");我们想异步调用FlightSearchService.searchFlights(),它返回一个List。让我们将其表示为Supplier>>:Supplier>flightSupplier=()->service.searchFlights(request);供应商>>origCompletionStageSupplier=()->CompletableFuture.supplyAsync(flightSupplier);然后我们可以使用TimeLimiter来装饰Supplier:ScheduledExecutorServicescheduler=Executors.newSingleThreadScheduledExecutor();Supplier>>decoratedCompletionStageSupplier=limiter.decorateCompletionStage;orscheduler)让我们调用修饰的异步操作:decoratedCompletionStageSupplier.get().whenComplete((result,ex)->{if(ex!=null){System.out.println(ex.getMessage());}if(result!=null){System.out.println(result);}});以下是成功航班搜索的示例输出,该搜索花费的时间少于我们指定的2秒timeoutDuration:Searchingforflights;当前时间=19:25:09783;当前线程=ForkJoinPool.commonPool-worker-3Flight搜索成功essful[Flight{flightNumber='XY765',flightDate='08/30/2020',from='NYC',to='LAX'},Flight{flightNumber='XY746',flightDate='08/30/2020',from='NYC',to='LAX'}]onthreadForkJoinPool.commonPool-worker-3以下是航班搜索超时的示例输出:Exceptionjava.util.concurrent.TimeoutException:TimeLimiter'flightSearch'在19:38:16963Searchingforflights记录了线程pool-1-thread-1的超时异常;当前时间=19:38:18448;currentthread=ForkJoinPool.commonPool-worker-3Flightsearchsuccessfulat19:38:18461上面的时间戳和线程名称表明,即使异步操作稍后在另一个线程上完成,调用线程也会收到TimeoutException如果我们要创建一个装饰器并在代码库的不同地方重用它,我们将使用decorateCompletionStage()。如果我们想创建它并立即执行Supplier,我们可以使用executeCompletionStage()实例方法代替:CompletionStage>decoratedCompletionStage=limiter.executeCompletionStage(scheduler,origCompletionStageSupplier);TimeLimiter事件TimeLimiter有一个EventPublisher,它会生成TimeLimiterOnSuccessEvent、TimeLimiterOnErrorEvent和TimeLimiterOnTimeoutEvent类型的Event。我们可以监听这些事件并记录它们,例如:限制器。getEventPublisher().onError(e->System.out.println(e.toString()));limiter.getEventPublisher().onTimeout(e->System.out.println(e.toString()));示例输出显示记录的内容:2020-08-07T11:31:48.181944:TimeLimiter'flightSearch'记录了一次成功的调用....其他行被省略...2020-08-07T11:31:48.582263:TimeLimiter'flightSearch'记录了超时异常。TimeLimiter指标TimeLimiter跟踪成功、失败和超时的调用次数。首先,我们像往常一样创建TimeLimiterConfig、TimeLimiterRegistry和TimeLimiter。然后,我们创建一个MeterRegistry并将TimeLimiterRegistry绑定到它:MeterRegistrymeterRegistry=newSimpleMeterRegistry();TaggedTimeLimiterMetrics.ofTimeLimiterRegistry(registry).bindTo(meterRegistry);在运行一些限时操作后,我们显示捕获的指标:ConsumermeterConsumer=meter->{Stringdesc=meter.getId().getDescription();StringmetricName=meter.getId().getName();StringmetricKind=meter.getId().getTag("种类");DoublemetricValue=StreamSupport.stream(meter.measure().spliterator(),false).filter(m->m.getStatistic().name().equals("COUNT")).findFirst().map(Measurement::getValue).orElse(0.0);System.out.println(desc+"-"+metricName+"("+metricKind+")"+":"+metricValue);};meterRegistry.forEachMeter(meterConsumer);这是一些示例输出:timedoutcalls-resilience4j.timelimiter.calls(timeout):6.0调用成功的次数-resilience4j.timelimiter.calls(ssuccessful):4.0调用失败的次数-resilience4j.timelimiter.calls(failed):0.0在实践中,我们定期将数据导出到监控系统并在仪表盘上进行分析实施时间限制时的陷阱和良好实践通常,我们处理两种操作——查询(或读取)和命令(或写入)。限时查询是安全的,因为我们知道它们不会改变系统的状态。我们看到的searchFlights()操作是查询操作的示例。命令通常会改变系统的状态。bookFlights()操作将是一个命令示例。在对命令进行时间限制时,我们必须记住,当我们超时时,该命令很可能仍在运行。例如,调用bookFlights()时出现TimeoutException并不一定意味着命令失败。在这种情况下,我们需要管理用户体验——也许在超时时我们可以通知用户操作花费的时间比我们预期的要长。然后我们可以向上游查询以检查操作状态并稍后通知用户。结论在本文中,我们学习了如何使用Resilience4j的TimeLimiter模块来设置异步、非阻塞操作的时间限制。我们通过一些实际示例了解了何时使用它以及如何配置它。您可以使用[在GitHub上](https://github.com/thombergs/…)的代码演示通过完整的应用程序来说明这些想法。本文翻译自:https://reflectoring.io/time-...