Java 項(xiàng)目中使用 Resilience4j 框架實(shí)現(xiàn)異步超時(shí)處理

image

到目前為止跳昼,在本系列中,我們已經(jīng)了解了 Resilience4j 及其 RetryRateLimiter 模塊艾杏。在本文中尔艇,我們將通過 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支持 FutureCompletableFuture。但是將它與 Future 一起使用相當(dāng)于 Future.get(long timeout, TimeUnit unit)毅桃。因此褒纲,我們將在本文的其余部分關(guān)注 CompletableFuture准夷。

與其他 Resilience4j 模塊一樣,TimeLimiter 的工作方式是使用所需的功能裝飾我們的代碼 - 如果在這種情況下操作未在指定的 timeoutDuration 內(nèi)完成莺掠,則返回 TimeoutException衫嵌。

我們?yōu)?TimeLimiter 提供 timeoutDurationScheduledExecutorService 和異步操作本身彻秆,表示為 CompletionStageSupplier楔绞。它返回一個(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香伴、TimeLimiterConfigTimeLimiterresilience4j-timelimiter 的主要抽象。

TimeLimiterRegistry 是用于創(chuàng)建和管理 TimeLimiter 對象的工廠具则。

TimeLimiterConfig 封裝了 timeoutDurationcancelRunningFuture 配置即纲。每個(gè) TimeLimiter 對象都與一個(gè) TimeLimiterConfig 相關(guān)聯(lián)。

TimeLimiter 提供輔助方法來為 FutureCompletableFuture 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虱而、TimeLimiterOnErrorEventTimeLimiterOnTimeoutEvent 類型的事件筏餐。我們可以監(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诅迷、TimeLimiterRegistryTimeLimiter佩番。然后,我們創(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/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子疯兼,更是在濱河造成了極大的恐慌然遏,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吧彪,死亡現(xiàn)場離奇詭異待侵,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)姨裸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進(jìn)店門秧倾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人傀缩,你說我怎么就攤上這事那先。” “怎么了赡艰?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵售淡,是天一觀的道長。 經(jīng)常有香客問我慷垮,道長揖闸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任料身,我火速辦了婚禮汤纸,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘芹血。我一直安慰自己贮泞,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布祟牲。 她就那樣靜靜地躺著,像睡著了一般抖部。 火紅的嫁衣襯著肌膚如雪说贝。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天慎颗,我揣著相機(jī)與錄音乡恕,去河邊找鬼。 笑死俯萎,一個(gè)胖子當(dāng)著我的面吹牛傲宜,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播夫啊,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼函卒,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了撇眯?” 一聲冷哼從身側(cè)響起报嵌,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤虱咧,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后锚国,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體腕巡,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年血筑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了绘沉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,505評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡豺总,死狀恐怖车伞,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情园欣,我是刑警寧澤帖世,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站沸枯,受9級特大地震影響日矫,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜绑榴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一哪轿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧翔怎,春花似錦窃诉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至容握,卻和暖如春宣脉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背剔氏。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工塑猖, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人谈跛。 一個(gè)月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓羊苟,卻偏偏與公主長得像,于是被迫代替她去往敵國和親感憾。 傳聞我的和親對象是個(gè)殘疾皇子蜡励,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,515評論 2 359

推薦閱讀更多精彩內(nèi)容