Dubbo原始碼分析(十)同步呼叫與非同步呼叫

清幽之地發表於2019-03-25

一、同步呼叫

預設情況下,我們通過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,進行接下來的處理。
複製程式碼

相關文章