「微服務(wù)設(shè)計(jì)之禪」重試模式
前言
微服務(wù)本質(zhì)上分布式架構(gòu),當(dāng)我們使用分布式系統(tǒng)時(shí)任何不可預(yù)知的問(wèn)題都會(huì)發(fā)生(例如網(wǎng)絡(luò)可用性問(wèn)題、服務(wù)可用性問(wèn)題、中間件可用性問(wèn)題)。一個(gè)系統(tǒng)的問(wèn)題可能會(huì)直接影響另外一個(gè)系統(tǒng)的使用或性能。所以在系統(tǒng)設(shè)計(jì)過(guò)程既要保證自身運(yùn)行的彈性需求,也要避免對(duì)下游服務(wù)級(jí)聯(lián)故障。
重試模式
在微服務(wù)技術(shù)架構(gòu)中,當(dāng)有多個(gè)服務(wù)(A,B,C ,D)時(shí),一個(gè)服務(wù)(A)可能依賴(lài)于另一服務(wù)(B),而另一服務(wù)(B)又可能依賴(lài)于 C,依此類(lèi)推。有時(shí)由于某些問(wèn)題,服務(wù) D 可能無(wú)法按預(yù)期響應(yīng)。服務(wù) D 可能引發(fā)了某些異常,例如 OutOfMemory Error 或 Internal Server Error。此類(lèi)異常會(huì)影響下游服務(wù),這可能導(dǎo)致用戶(hù)體驗(yàn)較差,如下所示。

類(lèi)似于網(wǎng)絡(luò)通信異常等,大部分情況通過(guò)刷新再次請(qǐng)求服務(wù)接口即可解決。在微服務(wù)架構(gòu)中,生產(chǎn)環(huán)境針對(duì)服務(wù) D 部署了多個(gè)實(shí)例,并通過(guò)負(fù)載均衡等實(shí)現(xiàn)服務(wù) D 高可用。如果其中一個(gè)實(shí)例網(wǎng)絡(luò)請(qǐng)求有問(wèn)題,無(wú)法響應(yīng)期望結(jié)果,我們可以通過(guò)重試該請(qǐng)求,通過(guò)負(fù)載均衡器將請(qǐng)求發(fā)送至運(yùn)行狀態(tài)良好的實(shí)例獲取正確結(jié)果。因此通過(guò)“重試”模式,可以使得應(yīng)用可用性得到保證。

示例程序
架構(gòu)圖

如上圖所示,簡(jiǎn)單模擬電商下單邏輯
用戶(hù)登錄瀏覽商品 (商品庫(kù)存模塊) 扣減商品庫(kù)存 (商品庫(kù)存模塊) 創(chuàng)建商品訂單 (訂單模塊)
product-service 通過(guò)調(diào)用 order-service 服務(wù)下單
代碼實(shí)現(xiàn)
├──?retry-demo
???├──?order-service????????#訂單服務(wù)??(8070)
???└──?product-service??????#商品庫(kù)存服務(wù)??(8050)
依賴(lài)說(shuō)明。由于 hystrix 年久失修,這里使用 resilience4j 斷路保護(hù)器做演示
<dependency>
????<groupId>io.github.resilience4jgroupId>
????<artifactId>resilience4j-spring-boot2artifactId>
????<version>1.6.1version>
dependency>
針對(duì)接口定義重試策略
resilience4j.retry:
??instances:
????ratingService:
??????maxRetryAttempts:?1?#重試策略
??????retryExceptions:???#針對(duì)哪些異常進(jìn)行重試
????????-?org.springframework.web.client.HttpServerErrorException
消費(fèi)方接口。product-service 8050
/**
?*?用戶(hù)點(diǎn)擊購(gòu)買(mǎi)
?*/
@SneakyThrows
@GetMapping("/order")
public?String?buy()?{
????//?模擬調(diào)用?訂單服務(wù)下單
????orderService.createOrder().get();
????return?"success";
}
定義遠(yuǎn)程調(diào)用類(lèi)使用 Retry 包裝
/**
?*?創(chuàng)建訂單
?*?name:?指定接口重試配置名稱(chēng)
?*?fallbackMethod:?重試后降級(jí)方法
?*/
@Bulkhead(name?=?"createOrder",?fallbackMethod?=?"getError")
public?CompletableFuture?createOrder()? {
????return?CompletableFuture.supplyAsync(()?->?restTemplate.getForEntity("http://localhost:8070/createOrder"
????????????,?String.class).getBody());
}
public?CompletableFuture?getError(Throwable?error)? {
????log.warn("創(chuàng)建訂單失敗了?{}",?error.getMessage());
????return?CompletableFuture.completedFuture("");
}
服務(wù)提供方。order-service 8070
@RestController
public?class?PayController?{
????private?static?int?num?=?0;
????@SneakyThrows
????@GetMapping("/createOrder")
????public?String?createOrder()?{
????????log.info("開(kāi)始請(qǐng)求創(chuàng)建訂單接口?{}",?++num);
????????//?請(qǐng)求次數(shù)奇數(shù)模擬創(chuàng)建訂單異常
????????if?(num?%?2?!=?0)?{
????????????throw?new?RuntimeException();
????????}
????????return?"創(chuàng)建訂單服務(wù)";
????}
}
開(kāi)始測(cè)試
請(qǐng)求商品服務(wù),返回成功
curl?http://localhost:8050/order
success?
訂單服務(wù)日志,由于是第一次請(qǐng)求觸發(fā)異常,然后服務(wù)調(diào)用方自動(dòng)重試產(chǎn)生第二次調(diào)用。
2020-12-06?15:50:07.664??INFO?25846?---?[nio-8070-exec-1]?c.e.o.controller.OrderController?????????:?開(kāi)始請(qǐng)求創(chuàng)建訂單接口?1
2020-12-06?15:50:07.686?ERROR?25846?---?[nio-8070-exec-1]?o.a.c.c.C.[.[.[/].[dispatcherServlet]????:?Servlet.service()?for?servlet?[dispatcherServlet]?in?context?with?path?[]?threw?exception?[Request?processing?failed;?nested?exception?is?java.lang.RuntimeException]?with?root?cause
...?異常日志?...
2020-12-06?15:50:08.271??INFO?25846?---?[nio-8070-exec-2]?c.e.o.controller.OrderController?????????:?開(kāi)始請(qǐng)求創(chuàng)建訂單接口?2
總結(jié)
重試模式可以通過(guò)編碼的形式自動(dòng)發(fā)起重試,避免終端用戶(hù)手動(dòng)刷新體驗(yàn)問(wèn)題。但重試模式的缺點(diǎn)是會(huì)造成整體的響應(yīng)時(shí)間,部分業(yè)務(wù)邏輯不能適用(比如冪等接口[1]等)
源碼:https://github.com/lltx/microservices-pattern[2]
參考資料和部分圖片來(lái)源 https://www.vinsguru.com[3]
參考資料
冪等接口: https://juejin.cn/post/6892966720017793037
[2]源碼:https://github.com/lltx/microservices-pattern: https://github.com/lltx/microservices-pattern
[3]參考資料和部分圖片來(lái)源 https://www.vinsguru.com: https://www.vinsguru.com
