到目前為止跳昼,在本系列中,我們已經(jīng)了解了 Resilience4j 及其 Retry 和 RateLimiter 模塊艾杏。在本文中尔艇,我們將通過 TimeLimiter 繼續(xù)探索 Resilience4j悦陋。我們將了解它解決了什么問題,何時(shí)以及如何使用它认烁,并查看一些示例。
代碼示例
本文附有 GitHub 上的工作代碼示例介汹。
什么是 Resilience4j却嗡?
請參閱上一篇文章中的描述,快速了解 Resilience4j 的一般工作原理嘹承。
什么是限時(shí)窗价?
對我們愿意等待操作完成的時(shí)間設(shè)置限制稱為時(shí)間限制。如果操作沒有在我們指定的時(shí)間內(nèi)完成赶撰,我們希望通過超時(shí)錯(cuò)誤收到通知舌镶。
有時(shí)柱彻,這也稱為“設(shè)定最后期限”。
我們這樣做的一個(gè)主要原因是確保我們不會讓用戶或客戶無限期地等待餐胀。不提供任何反饋的緩慢服務(wù)可能會讓用戶感到沮喪哟楷。
我們對操作設(shè)置時(shí)間限制的另一個(gè)原因是確保我們不會無限期地占用服務(wù)器資源。我們在使用 Spring 的 @Transactional
注解時(shí)指定的 timeout
值就是一個(gè)例子——在這種情況下否灾,我們不想長時(shí)間占用數(shù)據(jù)庫資源卖擅。
什么時(shí)候使用 Resilience4j TimeLimiter?
Resilience4j 的 TimeLimiter 可用于設(shè)置使用 CompleteableFutures
實(shí)現(xiàn)的異步操作的時(shí)間限制(超時(shí))墨技。
Java 8 中引入的 CompletableFuture
類使異步惩阶、非阻塞編程變得更容易】弁簦可以在不同的線程上執(zhí)行慢速方法断楷,釋放當(dāng)前線程來處理其他任務(wù)。 我們可以提供一個(gè)當(dāng) slowMethod()
返回時(shí)執(zhí)行的回調(diào):
int slowMethod() {
// time-consuming computation or remote operation
return 42;
}
CompletableFuture.supplyAsync(this::slowMethod)
.thenAccept(System.out::println);
這里的 slowMethod()
可以是一些計(jì)算或遠(yuǎn)程操作崭别。通常冬筒,我們希望在進(jìn)行這樣的異步調(diào)用時(shí)設(shè)置時(shí)間限制。我們不想無限期地等待 slowMethod()
返回茅主。例如舞痰,如果 slowMethod()
花費(fèi)的時(shí)間超過一秒,我們可能想要返回先前計(jì)算的诀姚、緩存的值响牛,甚至可能會出錯(cuò)。
在 Java 8 的 CompletableFuture
中赫段,沒有簡單的方法來設(shè)置異步操作的時(shí)間限制呀打。CompletableFuture
實(shí)現(xiàn)了 Future
接口,Future
有一個(gè)重載的 get()
方法來指定我們可以等待多長時(shí)間:
CompletableFuture<Integer> completableFuture = CompletableFuture
.supplyAsync(this::slowMethod);
Integer result = completableFuture.get(3000, TimeUnit.MILLISECONDS);
System.out.println(result);
但是這里有一個(gè)問題—— get()
方法是一個(gè)阻塞調(diào)用糯笙。所以它首先違背了使用 CompletableFuture 的目的聚磺,即釋放當(dāng)前線程。
這是 Resilience4j 的 TimeLimiter
解決的問題——它讓我們在異步操作上設(shè)置時(shí)間限制炬丸,同時(shí)保留在 Java 8 中使用 CompletableFuture
時(shí)非阻塞的好處瘫寝。
CompletableFuture
的這種限制已在 Java 9 中得到解決。我們可以在 Java 9 及更高版本中使用CompletableFuture
上的 orTimeout()
或 completeOnTimeout()
等方法直接設(shè)置時(shí)間限制稠炬。然而焕阿,憑借 Resilience4J
的 指標(biāo) 和 事件,與普通的 Java 9 解決方案相比首启,它仍然提供了附加值暮屡。
Resilience4j TimeLimiter 概念
TimeLimiter
支持 Future
和 CompletableFuture
。但是將它與 Future
一起使用相當(dāng)于 Future.get(long timeout, TimeUnit unit)
毅桃。因此褒纲,我們將在本文的其余部分關(guān)注 CompletableFuture
准夷。
與其他 Resilience4j 模塊一樣,TimeLimiter
的工作方式是使用所需的功能裝飾我們的代碼 - 如果在這種情況下操作未在指定的 timeoutDuration
內(nèi)完成莺掠,則返回 TimeoutException
衫嵌。
我們?yōu)?TimeLimiter
提供 timeoutDuration
、ScheduledExecutorService
和異步操作本身彻秆,表示為 CompletionStage
的 Supplier
楔绞。它返回一個(gè) CompletionStage
的裝飾 Supplier
。
在內(nèi)部唇兑,它使用調(diào)度器來調(diào)度一個(gè)超時(shí)任務(wù)——通過拋出一個(gè) TimeoutException
來完成 CompletableFuture
的任務(wù)酒朵。如果操作先完成,TimeLimiter
取消內(nèi)部超時(shí)任務(wù)扎附。
除了 timeoutDuration
之外蔫耽,還有另一個(gè)與 TimeLimiter
關(guān)聯(lián)的配置 cancelRunningFuture
。此配置僅適用于 Future
而不適用于 CompletableFuture留夜。當(dāng)超時(shí)發(fā)生時(shí)针肥,它會在拋出 TimeoutException
之前取消正在運(yùn)行的 Future
。
使用 Resilience4j TimeLimiter 模塊
TimeLimiterRegistry
香伴、TimeLimiterConfig
和 TimeLimiter
是 resilience4j-timelimiter 的主要抽象。
TimeLimiterRegistry
是用于創(chuàng)建和管理 TimeLimiter
對象的工廠具则。
TimeLimiterConfig
封裝了 timeoutDuration
和 cancelRunningFuture
配置即纲。每個(gè) TimeLimiter
對象都與一個(gè) TimeLimiterConfig
相關(guān)聯(lián)。
TimeLimiter
提供輔助方法來為 Future
和 CompletableFuture
Suppliers
創(chuàng)建或執(zhí)行裝飾器博肋。
讓我們看看如何使用 TimeLimiter
模塊中可用的各種功能低斋。我們將使用與本系列前幾篇文章相同的示例。假設(shè)我們正在為一家航空公司建立一個(gè)網(wǎng)站匪凡,以允許其客戶搜索和預(yù)訂航班膊畴。我們的服務(wù)與 FlightSearchService
類封裝的遠(yuǎn)程服務(wù)對話。
第一步是創(chuàng)建一個(gè) TimeLimiterConfig
:
TimeLimiterConfig config = TimeLimiterConfig.ofDefaults();
這將創(chuàng)建一個(gè) TimeLimiterConfig病游,其默認(rèn)值為 timeoutDuration
(1000ms) 和 cancelRunningFuture (true)唇跨。
假設(shè)我們想將超時(shí)值設(shè)置為 2s 而不是默認(rèn)值:
TimeLimiterConfig config = TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofSeconds(2))
.build();
然后我們創(chuàng)建一個(gè) TimeLimiter
:
TimeLimiterRegistry registry = TimeLimiterRegistry.of(config);
TimeLimiter limiter = registry.timeLimiter("flightSearch");
我們想要異步調(diào)用
FlightSearchService.searchFlights()
,它返回一個(gè) List<Flight>
衬衬。讓我們將其表示為 Supplier<CompletionStage<List<Flight>>>
:
Supplier<List<Flight>> flightSupplier = () -> service.searchFlights(request);
Supplier<CompletionStage<List<Flight>>> origCompletionStageSupplier =
() -> CompletableFuture.supplyAsync(flightSupplier);
然后我們可以使用 TimeLimiter
裝飾 Supplier
:
ScheduledExecutorService scheduler =
Executors.newSingleThreadScheduledExecutor();
Supplier<CompletionStage<List<Flight>>> decoratedCompletionStageSupplier =
limiter.decorateCompletionStage(scheduler, origCompletionStageSupplier);
最后买猖,讓我們調(diào)用裝飾的異步操作:
decoratedCompletionStageSupplier.get().whenComplete((result, ex) -> {
if (ex != null) {
System.out.println(ex.getMessage());
}
if (result != null) {
System.out.println(result);
}
});
以下是成功飛行搜索的示例輸出,其耗時(shí)少于我們指定的 2 秒 timeoutDuration
:
Searching for flights; current time = 19:25:09 783; current thread = ForkJoinPool.commonPool-worker-3
Flight search successful
[Flight{flightNumber='XY 765', flightDate='08/30/2020', from='NYC', to='LAX'}, Flight{flightNumber='XY 746', flightDate='08/30/2020', from='NYC', to='LAX'}] on thread ForkJoinPool.commonPool-worker-3
這是超時(shí)的航班搜索的示例輸出:
Exception java.util.concurrent.TimeoutException: TimeLimiter 'flightSearch' recorded a timeout exception on thread pool-1-thread-1 at 19:38:16 963
Searching for flights; current time = 19:38:18 448; current thread = ForkJoinPool.commonPool-worker-3
Flight search successful at 19:38:18 461
上面的時(shí)間戳和線程名稱表明滋尉,即使異步操作稍后在另一個(gè)線程上完成玉控,調(diào)用線程也會收到 TimeoutException。
如果我們想創(chuàng)建一個(gè)裝飾器并在代碼庫的不同位置重用它狮惜,我們將使用decorateCompletionStage()
高诺。如果我們想創(chuàng)建它并立即執(zhí)行 Supplier<CompletionStage>
碌识,我們可以使用 executeCompletionStage()
實(shí)例方法代替:
CompletionStage<List<Flight>> decoratedCompletionStage =
limiter.executeCompletionStage(scheduler, origCompletionStageSupplier);
TimeLimiter 事件
TimeLimiter
有一個(gè) EventPublisher
,它生成 TimeLimiterOnSuccessEvent
虱而、TimeLimiterOnErrorEvent
和 TimeLimiterOnTimeoutEvent
類型的事件筏餐。我們可以監(jiān)聽這些事件并記錄它們,例如:
TimeLimiter limiter = registry.timeLimiter("flightSearch");
limiter.getEventPublisher().onSuccess(e -> System.out.println(e.toString()));
limiter.getEventPublisher().onError(e -> System.out.println(e.toString()));
limiter.getEventPublisher().onTimeout(e -> System.out.println(e.toString()));
示例輸出顯示了記錄的內(nèi)容:
2020-08-07T11:31:48.181944: TimeLimiter 'flightSearch' recorded a successful call.
... other lines omitted ...
2020-08-07T11:31:48.582263: TimeLimiter 'flightSearch' recorded a timeout exception.
TimeLimiter 指標(biāo)
TimeLimiter
跟蹤成功薛窥、失敗和超時(shí)的調(diào)用次數(shù)胖烛。
首先,我們像往常一樣創(chuàng)建 TimeLimiterConfig
诅迷、TimeLimiterRegistry
和 TimeLimiter
佩番。然后,我們創(chuàng)建一個(gè) MeterRegistry
并將 TimeLimiterRegistry
綁定到它:
MeterRegistry meterRegistry = new SimpleMeterRegistry();
TaggedTimeLimiterMetrics.ofTimeLimiterRegistry(registry)
.bindTo(meterRegistry);
運(yùn)行幾次限時(shí)操作后罢杉,我們顯示捕獲的指標(biāo):
Consumer<Meter> meterConsumer = meter -> {
String desc = meter.getId().getDescription();
String metricName = meter.getId().getName();
String metricKind = meter.getId().getTag("kind");
Double metricValue =
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);
這是一些示例輸出:
The number of timed out calls - resilience4j.timelimiter.calls(timeout): 6.0
The number of successful calls - resilience4j.timelimiter.calls(successful): 4.0
The number of failed calls - resilience4j.timelimiter.calls(failed): 0.0
在實(shí)際應(yīng)用中趟畏,我們會定期將數(shù)據(jù)導(dǎo)出到監(jiān)控系統(tǒng)并在儀表板上進(jìn)行分析。
實(shí)施時(shí)間限制時(shí)的陷阱和良好實(shí)踐
通常滩租,我們處理兩種操作 - 查詢(或讀雀承恪)和命令(或?qū)懭耄Σ樵冞M(jìn)行時(shí)間限制是安全的律想,因?yàn)槲覀冎浪鼈儾粫淖兿到y(tǒng)的狀態(tài)猎莲。我們看到的 searchFlights()
操作是查詢操作的一個(gè)例子。
命令通常會改變系統(tǒng)的狀態(tài)技即。bookFlights()
操作將是命令的一個(gè)示例著洼。在對命令進(jìn)行時(shí)間限制時(shí),我們必須記住而叼,當(dāng)我們超時(shí)時(shí)身笤,該命令很可能仍在運(yùn)行。例如葵陵,bookFlights()
調(diào)用上的 TimeoutException
并不一定意味著命令失敗液荸。
在這種情況下,我們需要管理用戶體驗(yàn)——也許在超時(shí)時(shí)脱篙,我們可以通知用戶操作花費(fèi)的時(shí)間比我們預(yù)期的要長娇钱。然后我們可以查詢上游以檢查操作的狀態(tài)并稍后通知用戶。
結(jié)論
在本文中绊困,我們學(xué)習(xí)了如何使用 Resilience4j 的 TimeLimiter
模塊為異步忍弛、非阻塞操作設(shè)置時(shí)間限制。我們通過一些實(shí)際示例了解了何時(shí)使用它以及如何配置它考抄。
您可以使用 GitHub 上的代碼演示一個(gè)完整的應(yīng)用程序來說明這些想法细疚。
本文譯自:
https://reflectoring.io/time-limiting-with-resilience4j/