原創:Java派(微信公衆號:Java派),歡迎分享,轉載請保留出處。java
Dubbo
是阿里開源的RPC框架,由於他基於接口開發支持負載均衡、集羣容錯、版本控制等特性,所以如今有不少互聯網公司都在使用Dubbo。bash
本文主要解決使用超時設置以及處理進行分析,Dubbo有三個級別的超時設置分別爲:微信
具體設置方法可參考Dubbo的官方文檔。Dubbo調用超時後會發生啥狀況呢?目前瞭解的會有兩種狀況:app
TimeoutException
異常The timeout response finally returned at xxx
看起來還蠻正常的,可是實際上會有這樣問題:調用超時後服務端仍是會繼續執行,該如何處理呢? 爲了演示超時的狀況,先作了個服務:負載均衡
@Service(version = "1.0")
@Slf4j
public class DubboDemoServiceImpl implements DubboDemoService {
public String sayHello(String name) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
String result = "hello: " + name;
log.info("Result: {}" , result);
return result;
}
}
複製代碼
服務很是簡單,三秒後返回字符串。而後寫個controller調用它:框架
@RestController
@RequestMapping
public class DubboDemoController {
@Reference(url = "dubbo://127.0.0.1:22888?timeout=2000", version = "1.0")
private DubboDemoService demoService;
@GetMapping
public ResponseEntity<String> sayHello(@RequestParam("name") String name){
return ResponseEntity.ok(demoService.sayHello(name));
}
}
複製代碼
鏈接DubboDemoService
服務使用的直連方式(dubbo://127.0.0.1:22888?timeout=2000),演示中的超時時間都由url中的timeout指定。異步
前面提到發生調用超時後,客戶端會收到一個TimeoutException
異常,服務端的sayHello實現中是休眠了3秒的:ide
public String sayHello(String name) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
...
}
複製代碼
而鏈接服務時指定的超時時間是2000ms,那確定會收到一個TimeoutException
異常:ui
There was an unexpected error (type=Internal Server Error, status=500).
Invoke remote method timeout. method: sayHello, provider: dubbo://127.0.0.1:22888/com.example.dubbo.dubbodemo.service.DubboDemoService?application=dubbo-demo&default.check=false&default.lazy=false&default.sticky=false&dubbo=2.0.2&interface=com.example.dubbo.dubbodemo.service.DubboDemoService&lazy=false&methods=sayHello&pid=28662&qos.enable=false®ister.ip=192.168.0.103&remote.application=&revision=1.0&side=consumer&sticky=false&timeout=2000×tamp=1571800026289&version=1.0, cause: Waiting server-side response timeout. start time: 2019-10-23 11:13:00.745, end time: 2019-10-23 11:13:02.751, client elapsed: 5 ms, server elapsed: 2000 ms, timeout: 2000 ms, request: Request [id=4, version=2.0.2, twoway=true, event=false, broken=false, data=RpcInvocation [methodName=sayHello, parameterTypes=[class java.lang.String], arguments=[name], attachments={path=com.example.dubbo.dubbodemo.service.DubboDemoService, interface=com.example.dubbo.dubbodemo.service.DubboDemoService, version=1.0, timeout=2000}]], channel: /192.168.0.103:56446 -> /192.168.0.103:22888
複製代碼
客戶端超時處理比較簡單,既然發生了異常也能捕獲到異常那該回滾仍是不作處理,徹底能夠由開發者解決。url
try{
return ResponseEntity.ok(demoService.sayHello(name));
}catch (RpcException te){
//do something...
log.error("consumer", te);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR.value()).body("");
}
複製代碼
重點仍是解決服務方的超時異常。
Provider的處理就不像客戶端那樣簡單呢,由於Provider不會收到異常,並且線程也不會中斷,這樣就會致使Consumer超時數據回滾,而Providerder繼續執行最終執行完數據插入成功,數據不一致。
在演示項目中,Provider方法休眠3000ms且Consumer的超時是參數是2000ms,調用發生2000ms後就會發生超時,而Provider的sayHello方法不會中斷在1000ms後打印hello xx
。
很明顯要保持數據一致就須要在超時後,將Provider的執行終止或回滾才行,如何作到數據一致性呢?
Dubbo自身有重試機制,調用超時後會發起重試,Provider端需考慮冪等性。
使用補償事務或異步MQ保持最終一致性,須要寫一些與業務無關的代碼來保持數據最終一致性。好比在Provider端加個check方法,檢查是否成功,具體實現還須要結合自身的業務需求來處理。
@GetMapping
public ResponseEntity<String> sayHello(@RequestParam("name") String name){
try{
return ResponseEntity.ok(demoService.sayHello(name));
}catch (RpcException te){
//do something...
try{
demoService.check(name);
}catch (RpcException ignore){
}
log.error("consumer", te);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR.value()).body("");
}
}
複製代碼
雖然能夠經過添加檢查來驗證業務狀態,可是這個調用執行時間是沒辦法準確預知的,因此這樣簡單的檢測是效果不大,最好仍是經過MQ來作這樣的檢測。
原理比較簡單,在Consumer端調用時設置兩個參數ctime
、ttime
分別表示調用時間、超時時間,將參數打包發給Provider收到兩個參數後進行操做,若是執行時間越過ttime
則回滾數據,不然正常執行。改造下咱們的代碼:
public ResponseEntity<String> sayHello(@RequestParam("name") String name){
try{
RpcContext context = RpcContext.getContext();
context.setAttachment("ctime", System.currentTimeMillis() + "");
context.setAttachment("ttime", 2000 + "");
return ResponseEntity.ok(demoService.sayHello(name));
}catch (RpcException te){
//do something...
log.error("consumer", te);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR.value()).body("");
}
}
複製代碼
將ctime
、ttime
兩個參數傳到Provider端處理:
public String sayHello(String name) {
long curTime = System.currentTimeMillis();
String ctime = RpcContext.getContext().getAttachment("ctime");
String ttime = RpcContext.getContext().getAttachment("ttime");
long ctimeAsLong = Long.parseLong(ctime);
long ttimeAsLong = Long.parseLong(ttime);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
long spent = System.currentTimeMillis() - curTime;
if(spent >= (ttimeAsLong - ctimeAsLong - curTime)){
throw new RpcException("Server-side timeout.");
}
String result = "hello: " + name;
log.info("Result: {}" , result);
return result;
}
複製代碼
畫個圖看一下執行的時間線:
從上圖在執行完成後,響應返回期間這段時間是計算不出來的,因此這種辦法也不能徹底解決Provider超時問題。
文中提到的方法都不能很好的解決Provider超時問題,總的來講仍是要設計好業務代碼來減小調用時長,設置準確RPC調用的超時時間才能更好的解決這個問題。