一、同步呼叫
預設情況下,我們通過Dubbo呼叫一個服務,需得等服務端執行完全部邏輯,方法才得以返回。這個就是同步呼叫。
但大家是否考慮過另外一個問題,Dubbo底層網路通訊採用Netty,而Netty是非同步的;那麼它是怎麼將請求轉換成同步的呢?
首先我們來看請求方,在DubboInvoker
類中,它有三種不同的呼叫方式。
protected Result doInvoke(final Invocation invocation) throws Throwable {
try {
boolean isAsync = RpcUtils.isAsync(getUrl(), invocation);
boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
int timeout = getUrl().getMethodParameter(methodName, "timeout", 1000);
//忽略返回值
if (isOneway) {
boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);
currentClient.send(inv, isSent);
RpcContext.getContext().setFuture(null);
return new RpcResult();
//非同步呼叫
} else if (isAsync) {
ResponseFuture future = currentClient.request(inv, timeout);
RpcContext.getContext().setFuture(new FutureAdapter<Object>(future));
return new RpcResult();
//同步呼叫
} else {
RpcContext.getContext().setFuture(null);
return (Result) currentClient.request(inv, timeout).get();
}
}
}
複製程式碼
可以看到,上面的程式碼有三個分支,分別是:忽略返回值呼叫、非同步呼叫和同步呼叫。我們重點先看return (Result) currentClient.request(inv, timeout).get();
關於上面這句程式碼,它包含兩個動作:先呼叫currentClient.request
方法,通過Netty傳送請求資料;然後呼叫其返回值的get
方法,來獲取返回值。
1、傳送請求
這一步主要是將請求方法封裝成Request物件,通過Netty將資料傳送到服務端,然後返回一個DefaultFuture
物件。
public ResponseFuture request(Object request, int timeout) throws RemotingException {
//如果客戶端已斷開連線
if (closed) {
throw new RemotingException(".......");
}
//封裝請求資訊
Request req = new Request();
req.setVersion("2.0.0");
req.setTwoWay(true);
req.setData(request);
//構建DefaultFuture物件
DefaultFuture future = new DefaultFuture(channel, req, timeout);
try {
//通過Netty傳送網路資料
channel.send(req);
} catch (RemotingException e) {
future.cancel();
throw e;
}
return future;
}
複製程式碼
如上程式碼,邏輯很清晰。關於看它的返回值是一個DefaultFuture
物件,我們再看它的構造方法。
public DefaultFuture(Channel channel, Request request, int timeout) {
this.channel = channel;
this.request = request;
this.id = request.getId();
this.timeout = timeout > 0 ? timeout :
channel.getUrl().getPositiveParameter("timeout", 1000);
//當前Future和請求資訊的對映
FUTURES.put(id, this);
//當前Channel和請求資訊的對映
CHANNELS.put(id, channel);
}
複製程式碼
在這裡,我們必須先對Future有所瞭解。Future模式是多執行緒開發中非常常見的一種設計模式,在這裡我們返回這個物件後,呼叫其get方法來獲得返回值。
2、獲取返回值
我們接著看get方法。
public Object get(int timeout) throws RemotingException {
//設定預設超時時間
if (timeout <= 0) {
timeout = Constants.DEFAULT_TIMEOUT;
}
//判斷 如果操作未完成
if (!isDone()) {
long start = System.currentTimeMillis();
lock.lock();
try {
//通過加鎖、等待
while (!isDone()) {
done.await(timeout, TimeUnit.MILLISECONDS);
if (isDone() || System.currentTimeMillis() - start > timeout) {
break;
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
if (!isDone()) {
throw new TimeoutException(sent > 0, channel, getTimeoutMessage(false));
}
}
//返回資料
return returnFromResponse();
}
//獲取返回值response
private Object returnFromResponse() throws RemotingException {
Response res = response;
if (res == null) {
throw new IllegalStateException("response cannot be null");
}
if (res.getStatus() == Response.OK) {
return res.getResult();
}
if (res.getStatus() == 30 || res.getStatus() == 31) {
throw new TimeoutException(res.getStatus() == 31, channel, res.getErrorMessage());
}
throw new RemotingException(channel, res.getErrorMessage());
}
複製程式碼
如上程式碼,我們重點來看get
方法。我們總結下它的執行流程:
- 判斷超時時間,小於0則設定預設值
- 判斷操作是否已完成,即response是否為空;如果已完成,獲取返回值,並返回
- 如果操作未完成,加鎖、等待;獲得通知後,再次判斷操作是否完成。若完成,獲取返回值,並返回。
那麼我們就會想到兩個問題,response在哪裡被賦值、await在哪裡被通知。
在Netty讀取到網路資料後,其中會呼叫到HeaderExchangeHandler
中的方法,我們來看一眼就明白了。
public class HeaderExchangeHandler implements ChannelHandlerDelegate {
//處理返回資訊
static void handleResponse(Channel channel, Response response) throws RemotingException {
if (response != null && !response.isHeartbeat()) {
DefaultFuture.received(channel, response);
}
}
}
複製程式碼
上面說的很清楚,如果response 不為空,並且不是心跳資料,就呼叫DefaultFuture.received
,在這個方法裡面,主要就是根據返回資訊的ID找到對應的Future,然後通知。
public static void received(Channel channel, Response response)
try {
//根據返回資訊中的ID找到對應的Future
DefaultFuture future = FUTURES.remove(response.getId());
if (future != null) {
//通知方法
future.doReceived(response);
} else {
logger.warn("......");
}
} finally {
//處理完成,刪除Future
CHANNELS.remove(response.getId());
}
}
複製程式碼
future.doReceived(response);
就很簡單了,它就回答了我們上面的那兩個小問題。賦值response和await通知。
private void doReceived(Response res) {
lock.lock();
try {
//賦值response
response = res;
if (done != null) {
//通知方法
done.signal();
}
} finally {
lock.unlock();
}
if (callback != null) {
invokeCallback(callback);
}
}
複製程式碼
通過以上方式,Dubbo就完成了同步呼叫。我們再總結下它的整體流程:
- 將請求封裝為Request物件,並構建DefaultFuture物件,請求ID和Future對應。
- 通過Netty傳送Request物件,並返回DefaultFuture物件。
- 呼叫
DefaultFuture.get()
等待資料回傳完成。 - 服務端處理完成,Netty處理器接收到返回資料,通知到DefaultFuture物件。
- get方法返回,獲取到返回值。
二、非同步呼叫
如果想使用非同步呼叫的方式,我們就得配置一下。在消費者端配置檔案中
<dubbo:reference id="infoUserService"
interface="com.viewscenes.netsupervisor.service.InfoUserService"
async="true"/>
複製程式碼
然後我們再看它的實現方法
if (isAsync) {
ResponseFuture future = currentClient.request(inv, timeout);
RpcContext.getContext().setFuture(new FutureAdapter<Object>(future));
return new RpcResult();
}
複製程式碼
可以看到,它同樣是通過currentClient.request
返回的Future物件,但並未呼叫其get方法;而是將Future物件封裝成FutureAdapter,然後設定到RpcContext.getContext()
RpcContext是Dubbo中的一個上下文資訊,它是一個 ThreadLocal 的臨時狀態記錄器。我們重點看它的setFuture
方法。
public class RpcContext {
private static final ThreadLocal<RpcContext> LOCAL = new ThreadLocal<RpcContext>() {
@Override
protected RpcContext initialValue() {
return new RpcContext();
}
};
private Future<?> future;
public void setFuture(Future<?> future) {
this.future = future;
}
}
複製程式碼
既然它是基於ThreadLocal機制實現,那麼我們在獲取返回值的時候,通過ThreadLocal獲取到上下文資訊物件,再拿到其Future物件就好了。這個時候,我們客戶端應該這樣來做
userService.sayHello("Jack");
Future<Object> future = RpcContext.getContext().getFuture();
System.out.println("服務返回訊息:"+future.get());
複製程式碼
這樣做的好處是,我們不必等待在單一方法上,可以呼叫多個方法,它們會並行的執行。比如像官網給出的例子那樣:
// 此呼叫會立即返回null
fooService.findFoo(fooId);
// 拿到呼叫的Future引用,當結果返回後,會被通知和設定到此Future
Future<Foo> fooFuture = RpcContext.getContext().getFuture();
// 此呼叫會立即返回null
barService.findBar(barId);
// 拿到呼叫的Future引用,當結果返回後,會被通知和設定到此Future
Future<Bar> barFuture = RpcContext.getContext().getFuture();
// 此時findFoo和findBar的請求同時在執行,客戶端不需要啟動多執行緒來支援並行,而是藉助NIO的非阻塞完成
// 如果foo已返回,直接拿到返回值,否則執行緒wait住,等待foo返回後,執行緒會被notify喚醒
Foo foo = fooFuture.get();
// 同理等待bar返回
Bar bar = barFuture.get();
// 如果foo需要5秒返回,bar需要6秒返回,實際只需等6秒,即可獲取到foo和bar,進行接下來的處理。
複製程式碼