同步调用
同步调用是一种阻塞式的调用方式,即 Consumer 端代码一直阻塞等待,直到 Provider 端返回为止;
dubbo默认的协议是netty, Netty 是NIO 异步通讯机制,那么服务调用是怎么转化为同步的呢?
下面看源码:
省略一部分调用链,最终会来到这里 DubboInvoker
protected Result doInvoke(final Invocation invocation) throws Throwable {
RpcInvocation inv = (RpcInvocation) invocation;
final String methodName = RpcUtils.getMethodName(invocation);
// 设置 path 和 version 到 attachment 中
inv.setAttachment(Constants.PATH_KEY, getUrl().getPath());
inv.setAttachment(Constants.VERSION_KEY, version);
ExchangeClient currentClient;
if (clients.length == 1) {
// 从 clients 数组中获取 ExchangeClient
currentClient = clients[0];
} else {
currentClient = clients[index.getAndIncrement() % clients.length];
}
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();
}
}
}
接着我们看同步调用部分,(Result) currentClient.request(inv, timeout).get();
关于上面这句代码,它包含两个动作:先调用currentClient.request
方法,通过Netty发送请求数据;然后调用其返回值的get
方法,来获取返回值。
1、发送请求
这一步主要是将请求方法封装成Request对象,通过Netty将数据发送到服务端,然后返回一个DefaultFuture
对象。
com.alibaba.dubbo.remoting.exchange.support.header.HeaderExchangeChannel的Request方法:
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);
}
同时类加载的时候会启动一个超时扫描线程:
static {
Thread th = new Thread(new RemotingInvocationTimeoutScan(), "DubboResponseTimeoutScanTimer");
th.setDaemon(true);
th.start();
}
看看线程扫描啥:
private static class RemotingInvocationTimeoutScan implements Runnable {
@Override
public void run() {
while (true) {
try {
// 就是去扫描DefaultFuture列表
for (DefaultFuture future : FUTURES.values()) {
if (future == null || future.isDone()) {
continue;
}
// 如果future未完成,且超时
if (System.currentTimeMillis() - future.getStartTimestamp() > future.getTimeout()) {
// 创建一个异常的Response
Response timeoutResponse = new Response(future.getId());
// set timeout status.
timeoutResponse.setStatus(future.isSent() ? Response.SERVER_TIMEOUT : Response.CLIENT_TIMEOUT);
timeoutResponse.setErrorMessage(future.getTimeoutMessage(true));
// 处理异常
DefaultFuture.received(future.getChannel(), timeoutResponse);
}
}
Thread.sleep(30);
} catch (Throwable e) {
logger.error("Exception when scan the timeout invocation of remoting.", e);
}
}
}
}
2、获取返回值
我们接着看get方法。com.alibaba.dubbo.remoting.exchange.support.DefaultFuture#get()
public Object get() throws RemotingException {
return get(timeout);
}
com.alibaba.dubbo.remoting.exchange.support.DefaultFuture#get(int)
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");
}
// 正常返回,返回 Result 对象
if (res.getStatus() == Response.OK) {
return res.getResult();
}
// 超时处理
if (res.getStatus() == Response.CLIENT_TIMEOUT || res.getStatus() == Response.SERVER_TIMEOUT) {
// 重新抛出异常
throw new TimeoutException(res.getStatus() == Response.SERVER_TIMEOUT, channel, res.getErrorMessage());
}
throw new RemotingException(channel, res.getErrorMessage());
}
其中的isDone()方法为:
public boolean isDone() {
return this.response != null;
}
我们总结下它的运行流程:
- 判断超时时间,小于0则设置默认值
- 判断操作是否已完成,即response是否为空;如果已完成,获取返回值,并返回
- 如果操作未完成,加锁、等待;获得通知后,再次判断操作是否完成。若完成,获取返回值,并返回。
其中等待用的是 done.await(timeout, TimeUnit.MILLISECONDS);
done为private final Condition done;
因此调用的是Condition的await方法,会释放锁和资源,singl方法会将其唤醒。
get方法后面的returnFromResponse()方法可以看到超时处理,也就是之前那个超时扫描线程对status进行赋值后,returnFromResponse()方法里面对超时扫描线程赋值的status值进行判断是否超时,如果超时就抛出TimeoutException异常
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);
}
}
异步调用的话用到了RpcContext.
RpcContext是Dubbo中的一个上下文信息,它是一个 ThreadLocal 的临时状态记录器