Dubbo笔记 ⑳ :消费者的异步调用

一、前言

本系列为个人Dubbo学习笔记,内容基于《深度剖析Apache Dubbo 核心技术内幕》, 过程参考官方源码分析文章,仅用于个人笔记记录。本文分析基于Dubbo2.7.0版本,由于个人理解的局限性,若文中不免出现错误,感谢指正。


从2.7.0版本开始,Dubbo以CompletableFuture为基础支持所有异步编程接口,解决了2.7.0之前的版本异步调用的不便与功能缺失。需要注意的 Dubbo2.7.0 之前的代码和 2.7.0 之后的代码有所差异,本文基于Dubbo2.7.0 的代码分析。


对于Dubbo的异步过程来说,存在消费者和提供者两方的异步:

  • 对于消费者,即异步调用 : 消费者发起服务后并不阻塞本地线程,可以通过回调方式或者 阻塞线程方式来获取异步调用的结果。异步调用是基于NIO 的非阻塞能力实现并行调用,服务消费端不需要启动多线程即可完成并行调用多个远程服务,相对多线程开销较小。
  • 对于提供者,即异步执行 :在Provider端非异步执行时,对调用方发来的请求的处理是在Dubbo内部线程模型的线程池里的线程中执行的。在Dubbo中,服务提供方提供的所有服务接口都是使用这一个线程池来执行的,所以当一个服务执行比较耗时时,可能会占用线程池中很多线程,从而导致其他服务的处理收到影响。Provider端异步执行则将服务的处理逻辑从Dubbo内部线程池切换到业务自定义线程,避免Dubbo线程池中的线程被过度占用,有助于避免不同服务间的互相影响。但需要注意的是,Provider端异步执行对节省资源和提升RPC响应性能是没有效果的,这是因为如果服务处理比较耗时,虽然不是使用Dubbo框架内部线程处理,但还是需要业务自己的线程来处理。

系列文章:

  1. Dubbo笔记 ⑳ :消费者的异步调用
  2. Dubbo笔记 ㉑ :提供者的异步执行

衍生篇:

  1. Dubbo衍生篇⑦ :异步场景下的问题

1. 流程概述

以下内容来源于 《深度剖析Apache Dubbo 核心技术内幕》:

下图显示的是Dubbo异步调用链路的流程图。
在这里插入图片描述
图中的实线箭头代表同步调用,虚线箭头表示异步调用。

图中步骤1是当服务消费端发起RPC调用时使用的用户线程,用户线程首先使用步骤2创建一个Future对象,接着步骤3会把请求转换为I/O线程来执行,步骤3为异步过程,所以会马上返回,然后用户线程使用步骤4把其创建的Future对象设置到RpcContext中,其后用户线程就返回了。在步骤5中,用户线程可以在某个时间点从RpcContext中获取设置的Future对象,并且使用步骤6来等待调用结果。在步骤7中,当服务提供方返回结果后,调用方线程模型中的线程池中的线程则会把结果使用步骤8写入Future,这时用户线程就可以得到远程调用结果了。


个人总结:
当消费者发起异步调用后,消费者端将请求发送后并不会等待请求返回,而是会创建一个 Future 对象,并将其保存到 RpcContext 中,随后返回结果(这个结果为同步请求结果,一般为无意义的null值)。这样,业务线程不需要等待请求结束,而是可以继续后面的业务逻辑。如果需要获取异步请求结果,可以通过 Future#get 或者设置完成回调的方式来获取(通过 Future#get 获取时会阻塞当前线程)。当异步请求结束后,提供者将结果写回到 Future 中。消费者便可以获取到。

二、 关键类

异步调用的步骤比较繁杂,所以这里先将部关键类和方法抽出来讲解。可以在直接看后面调用过程的分析,当涉及到下面类和方法时再回过头来看。

1. DefaultFuture

在这里插入图片描述
DefaultFuture 是 Dubbo 自定义的类,实现了 ResponseFuture 接口。完成了 请求结果的接收以及结果回调等功能。可以认为 一个 DefaultFuture 对象对应一次异步调用。

个人认为 DefaultFuture 是整个调用过程的核心之一:

  • 当一个异步请求发起时会创建一个 DefaultFuture 并被缓存, DefaultFuture 中保存这次请求的相关信息,包括 Request、Channel 、超时时间等。也即是说 每一次异步请求对应一个唯一的DefaultFuture 对象
  • 当提供者将结果返回时,消费者端会触发DefaultFuture#received 方法,通过请求id找到本次请求对应的 DefaultFuture 实例,来将请求结果保存,如果设置了完成回调,则会调用完成回调(FutureAdapter 则是通过完成回调完成自身的更新)。

1.1 DefaultFuture 的构造

下面是 DefaultFuture 的一些属性和构造方法:

    private static final Logger logger = LoggerFactory.getLogger(DefaultFuture.class);
	// 缓存通信通道 :key为 request Id 用于标识请求, Value 为这次请求会和提供者之间的Channel。
    private static final Map<Long, Channel> CHANNELS = new ConcurrentHashMap<>();
	// 缓存future :key 为request Id, Value 为这次请求的 DefaultFuture
    private static final Map<Long, DefaultFuture> FUTURES = new ConcurrentHashMap<>();
	// 超时检查定时器。
    public static final Timer TIME_OUT_TIMER = new HashedWheelTimer(
            new NamedThreadFactory("dubbo-future-timeout", true),
            30,
            TimeUnit.MILLISECONDS);
	
    // invoke id.
    // 请求 id,requestId
    private final long id;
    // 当前请求的通道
    private final Channel channel;
    // 当前请求
    private final Request request;
    // 请求超时时间
    private final int timeout;
    // 独占锁
    private final Lock lock = new ReentrantLock();
    // lock 的条件变量
    private final Condition done = lock.newCondition();
    // 开始时间,用于计算请求是否超时
    private final long start = System.currentTimeMillis();
    private volatile long sent;
    // 请求的返回内容
    private volatile Response response;
    // 返回回调
    private volatile ResponseCallback callback;
	// 构造函数,进行部分属性的初始化赋值
    private DefaultFuture(Channel channel, Request request, int timeout) {
        this.channel = channel;
        this.request = request;
        // request id  请求唯一标识
        this.id = request.getId();
        // 设置超时时间,如果timeout < 0 则使用默认的 1000ms
        this.timeout = timeout > 0 ? timeout : channel.getUrl().getPositiveParameter(Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT);
        // put into waiting map.
        // 当前请求放入缓存
        FUTURES.put(id, this);
        CHANNELS.put(id, channel);
    }

这里需要注意的是:

  1. CHANNELSFUTURES 为 常量,即,所有的DefaultFuture 都共享这两个变量。当发起一个异步请求后,会创建一个 DefaultFuture。并使用本次请求的请求id作为key缓存在 FUTURES 中。同理,一次异步请求需要连接一个通道 Channel ,也会缓存到 CHANNELS中。

    个人理解: CHANNELSFUTURES保存着所有未结束的异步请求信息,当请求发起时,会创建一个 DefaultFuture,DefaultFuture中保存着这次请求的相关信息。而在 DefaultFuture 构造函数中会将这次请求的 Future 和 channel 保存到 CHANNELSFUTURES 中,所以每一个请求会对应一个 DefaultFuture。当请求结果写回时,DefaultFuture#received 根据请求id找到对应请求的Future 和 channel 来做进一步处理(如结果写回、回调执行等);当请求结束后,会清除 CHANNELSFUTURES 中关于这次请求的信息。

  2. lock和done是为了实现线程之间的通知等待模型,比如调用DefaultFuture的get()方法的线程为了获取响应结果,内部会调用done.await()方法挂起调用线程。当接收到响应结果后,调用方线程模型中线程池里的线程会调用received()方法,其内部会把响应结果设置到DefaultFuture内,然后调用done的signal()方法激活一个因调用done的await()系列方法而挂起的线程(比如调用get()方法被阻塞的线程)

1.2 DefaultFuture#newFuture

消费者在创建 DefaultFuture 时并非直接通过构造函数创建,而是通过 DefaultFuture#newFuture。在 HeaderExchangeChannel#request 中通过 DefaultFuture#newFuture 来创建 DefaultFuture。其实现如下:

    public static DefaultFuture newFuture(Channel channel, Request request, int timeout) {
        final DefaultFuture future = new DefaultFuture(channel, request, timeout);
        // timeout check
        // 请求超时检查器
        timeoutCheck(future);
        return future;
    }
 	/**
     * check time out of the future
     */
    private static void timeoutCheck(DefaultFuture future) {
    	// 创建一个任务
        TimeoutCheckTask task = new TimeoutCheckTask(future);
        // 定时 time 执行任务
        TIME_OUT_TIMER.newTimeout(task, future.getTimeout(), TimeUnit.MILLISECONDS);
    }

  	private static class TimeoutCheckTask implements TimerTask {

        private DefaultFuture future;

        TimeoutCheckTask(DefaultFuture future) {
            this.future = future;
        }

        @Override
        public void run(Timeout timeout) {
        	// 如果future = null || 请求结束,则直接返回
            if (future == null || future.isDone()) {
                return;
            }
            // create exception 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));
            // handle response.
            // 把超时响应信息设置到 Future 内的通道,以结束此次请求
            DefaultFuture.received(future.getChannel(), timeoutResponse);

        }
    }

可以看到 DefaultFuture#newFuture 创建了一个 DefaultFuture对象,并启动了一个定时器,当异步请求超时时间后检查是否已经有响应结果,如果有则直接返回,否则返回超时信息。


这里我们可以看到如果请求超时,这里是调用 DefaultFuture#received 来结束请求的。
实际上并不仅仅是超时,当请求正常返回时也会调用 DefaultFuture#received 来讲结果写回,该部分流程如下:

当提供者将请求结果发送会消费者时消费者端会触发
	HeaderExchangeHandler#received 
	  ->  HeaderExchangeHandler#handleResponse 
			->  DefaultFuture.received

1.3 DefaultFuture#received

异步请求结果返回异步请求超时 后,都会调用 DefaultFuture#received 方法。

  • 当异步请求正常结束时,调用流程如下:
    当提供者将请求结果发送给消费者时,消费者端会触发
    HeaderExchangeHandler#received 
      ->  HeaderExchangeHandler#handleResponse 
    		->  DefaultFuture.received
    
  • 当异步调用超时时,在 DefaultFuture#newFuture 创建时添加的定时器会启动,会调用 DefaultFuture#received 方法,把超时响应信息设置到 Future 内的通道,以结束此次请求。

下面我们来看一下 DefaultFuture#received 方法 的实现(注意 DefaultFuture#received 是一个静态方法):

    public static void received(Channel channel, Response response) {
        try {
        	// 从 FUTURES 移除请求id对应的 DefaultFuture (在DefaultFuture 创建时将其缓存到FUTURES 中  )
            DefaultFuture future = FUTURES.remove(response.getId());
            if (future != null) {
            	// 交由 future 来处理响应体
                future.doReceived(response);
            } else {
              // ...  日志打印
            }
        } finally {
        	// 通道移除
            CHANNELS.remove(response.getId());
        }
    }

	 private void doReceived(Response res) {
 		// 获取独占锁
        lock.lock();
        try {
        	// 1. 赋值给 response 。在 DefaultFuture#get 中判断请求是否结束就是通过response  属性是否为空判断
            response = res;
            if (done != null) {
            	// 2. 唤醒一个由于 done.wait 而阻塞的线程
                done.signal();
            }
        } finally {
        	// 释放锁
            lock.unlock();
        }
        // 3. 如果设置了回调,则调用回调
        if (callback != null) {
            invokeCallback(callback);
        }
    }

我们这里主要看 DefaultFuture#doReceived 的过程:

  1. DefaultFuture#received 方法将响应结果设置到 DefaultFuture#response 变量中(此时的响应结果可能是正常的响应结果,也可能是超时响应)。
  2. 通过 done.signal(); 唤醒了由于 done.wait 而阻塞的线程。
  3. 如果设置了回调,则调用DefaultFuture#invokeCallback 进行回调处理。

到这里我们会有疑问: done.wait 是什么时候调用阻塞线程的?实际上是在 DefaultFuture#get 方法中。

1.4 DefaultFuture#get

这里我们需要看一下 DefaultFuture#get 方法。DefaultFuture#get用于获取异步调用的结果,如果请求尚未完成则阻塞当前线程。其实现如下:

	// 用于判断请求是否结束,判断依据在于 response  是否为 null 
    @Override
    public boolean isDone() {
        return response != null;
    }
    
	@Override
    public Object get() throws RemotingException {
        return get(timeout);
    }

    @Override
    public Object get(int timeout) throws RemotingException {
        if (timeout <= 0) {
            timeout = Constants.DEFAULT_TIMEOUT;
        }
        // 如果future 任务仍未结束,判断条件就是  response != null
        if (!isDone()) {
            long start = System.currentTimeMillis();
            lock.lock();
            try {
            	// while 循环等待请求结束,当请求结束 || 时间超时,跳出循环
                while (!isDone()) {
                	// 等待 timeout时间
                    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();
    }

    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() == 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());
    }

上面代码流程可以简述为 :

  1. 如果超时时间<=0,则使用默认值1000;

  2. 如果任务还没完成(响应结果还是为null),则调用条件变量done的await()方法,让当前线程挂起timeout时间

  3. 若在timeout时间内得到了响应结果(也就是说received()方法被调用了),则当前线程会被激活;如果已经得到响应结果,则将结果返回。

  4. DefaultFuture#get 在 DubboInvoker#doInvoke 中会被调用。当同步请求 && 需要返回值时会调用该方法。如下图:
    在这里插入图片描述

1.5 DefaultFuture#setCallback

我们上面讲过当请求结束时会调用 DefaultFuture#received 方法,而在 DefaultFuture#received 方法中,如果当前 Future 设置了 回调,则会触发回调方法。而回调方法则是通过 DefaultFuture#setCallback 进行的设置。其实现如下:

    @Override
    public void setCallback(ResponseCallback callback) {
    	// 如果请求完成,则直接调用回调方法
    	// isDone 判断条件是  response != null; 如果请求结束,则会将请求结果赋值给 response,从而使得 isDone 返回true
        if (isDone()) {
        	// 执行回调方法
            invokeCallback(callback);
        } else {
        	// 到这里说明请求尚未结束
            boolean isdone = false;
            lock.lock();
            try {
            	// 保存回调方法,等到请求结束后再执行回调方法
                if (!isDone()) {
                    this.callback = callback;
                } else {
                    isdone = true;
                }
            } finally {
                lock.unlock();
            }
            // 再次确认请求完成,调用回调
            if (isdone) {
                invokeCallback(callback);
            }
        }
    }

这里看到整个逻辑很简单:

  1. 如果调用 setCallback 方法时请求已经结束,则直接执行回调方法。
  2. 如果当前请求没有结束,则将 回调方法保存在 callback 属性中
  3. 最后再判断一次,请求是否完成,完成则直接进行回调。


对于消费者端,在创建 FutureAdapter 的时候为入参的时候设置了回调方法,提供者则不涉及该部分回调内容。

1.6 DefaultFuture#invokeCallback

DefaultFuture#invokeCallback 是执行回调方法。逻辑也比较清楚。如下:

	// 执行回调方法
	private void invokeCallback(ResponseCallback c) {
        ResponseCallback callbackCopy = c;
        // 回调方法为空直接抛异常
        if (callbackCopy == null) {
            throw new NullPointerException("callback cannot be null.");
        }
        c = null;
        Response res = response;
        // 请求结果为空抛出异常
        if (res == null) {
            throw new IllegalStateException("response cannot be null. url:" + channel.getUrl());
        }
		// 如果响应状态正常
        if (res.getStatus() == Response.OK) {
            try {
            	// 将响应结果回调给 done 方法
                callbackCopy.done(res.getResult());
            } catch (Exception e) {
                logger.error("callback invoke error .result:" + res.getResult() + ",url:" + channel.getUrl(), e);
            }
        } else if (res.getStatus() == Response.CLIENT_TIMEOUT || res.getStatus() == Response.SERVER_TIMEOUT) {
        	// 响应超时 回调
            try {
                TimeoutException te = new TimeoutException(res.getStatus() == Response.SERVER_TIMEOUT, channel, res.getErrorMessage());
                callbackCopy.caught(te);
            } catch (Exception e) {
                logger.error("callback invoke error ,url:" + channel.getUrl(), e);
            }
        } else {
            try {
            	// 异常回调
                RuntimeException re = new RuntimeException(res.getErrorMessage());
                callbackCopy.caught(re);
            } catch (Exception e) {
                logger.error("callback invoke error ,url:" + channel.getUrl(), e);
            }
        }
    }

这里的逻辑也比较简单:ResponseCallback 有两个方法 ResponseCallback#done 和 ResponseCallback#caught。当请求成功时调用ResponseCallback#done,失败则调用 ResponseCallback#caught。消费者通过 对 FutureAdapter#future 的回调感知到了异步调用结束。


2. FutureAdapter

FutureAdapter 是 CompletableFuture 的一个适配类,其结构如下:
在这里插入图片描述

FutureAdapter 继承了 CompletableFuture,作为一个 Future 适配器类用来包装 DefaultFuture。对消费者来说,异步调用时通过 RpcContext.getContext().getFuture() 获取到的Future 就是 FutureAdapter。

当提供者将结果返回时,消费者触发DefaultFuture#received 方法,通过请求id找到本次请求对应的 DefaultFuture 实例,来将请求结果保存,如果设置了完成回调,则会调用完成回调。而 FutureAdapter 则在构造函数中设置了回调,在其回调方法中更新了 FutureAdapter.result 属性。

可以认为 一个 FutureAdapter 对象 和一个 DefaultFuture 对象同样对应一次异步调用(FutureAdapter 中保存了 DefaultFuture 对象)。


下面我们来看 FutureAdapter 的部分实现:

2.1 FutureAdapter 构造函数

FutureAdapter 在构造函数中对 ResponseFuture 设置了回调方法。

public class FutureAdapter<V> extends CompletableFuture<V> {

    private final ResponseFuture future;
    private CompletableFuture<Result> resultFuture;

    public FutureAdapter(ResponseFuture future) {
        this.future = future;
        this.resultFuture = new CompletableFuture<>();
        // 设置回调。当请求完成后会通过进行回调
        future.setCallback(new ResponseCallback() {
            @Override
            public void done(Object response) {
            	// 对请求结果进行处理
                Result result = (Result) response;
       			// 1. 完成对 result 的 操作,触发消费者端的过滤器链
                FutureAdapter.this.resultFuture.complete(result);
                V value = null;
                try {
                	// 2. 调用 Result#recreate 获取获取结果值
                    value = (V) result.recreate();
                } catch (Throwable t) {
                	// 3. 异常情况的处理
                    FutureAdapter.this.completeExceptionally(t);
                }
                // 4. 完成对 value 进行进一步处理,如用户线程的回调
                FutureAdapter.this.complete(value);
            }

            @Override
            public void caught(Throwable exception) {
                FutureAdapter.this.completeExceptionally(exception);
            }
        });
    }
 	// .... 省略部分代码
}

我们按照上面代码的注释步骤来讲解:

  1. 这里触发了 FutureAdapter#resultFuture 的操作,这一部分完成最重要的作用是 触发异步调用时的过滤器链执行。 关于过滤器链的执行问题,详参Dubbo衍生篇⑦ :异步场景下的问题

  2. 调用 Result#recreate 获取获取结果值 value。
    我们这里来看一下 Dubbo的一次服务调用的时序图(图源《深度剖析Apache Dubbo 核心技术内幕》):在这里插入图片描述
    对于同步调用,DubboInvoker#doInvoke 会返回一个 RpcResult,而在InvokerInvocationHandler#invoke 中 会调用 RpcResult#recreate 方法来最后一次处理一次返回值,所以最终返回给消费者的结果是 RpcResult#recreate() 返回的结果。

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            String methodName = method.getName();
            Class<?>[] parameterTypes = method.getParameterTypes();
         	....
    		// 调用了 Result#recreate 来对返回结果进行处理。
            return invoker.invoke(createInvocation(method, args)).recreate();
        }
    

    而对于异步调用其返回值自然也需要调用 RpcResult#recreate() 方法。所以当异步调用结果返回时(即当前这一步),调用 RpcResult#recreate() 来完成对结果的最后一次处理。

  3. 如果出现异常,则调用异常回调,解释同步骤四

  4. 如果正常结束,则调用正常回调。通常情况下,用户线程在发起异步调用后会使用 CompletableFuture#whenComplete 来设置回调,当任务执行结束会回调该方法来让用户线程感知到异步调用结束。而这里的 CompletableFuture,一般是通过 Dubbo上下文获取的 FutureAdapter。所以这里调用 FutureAdapter.this.complete(value); 会触发用户线程的 CompletableFuture#whenComplete,完成用户感知结果的功能。

2.2 FutureAdapter#get

FutureAdapter#get 则是调用 父类的 get 方法完成了异步调用。其实现如下,可以看到关键逻辑在于super.get(timeout, unit);,即调用了父类的get 方法。

    @Override
    @SuppressWarnings("unchecked")
    public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
        try {
            return super.get(timeout, unit);
        } catch (TimeoutException | ExecutionException | InterruptedException e) {
            throw e;
        } catch (Throwable e) {
            throw new RpcException(e);
        }
    }
    
 	// .... 省略部分代码

FutureAdapter#get 直接调用父类的方法,即 CompletableFuture#get,若此时请求尚未结束,则不会有结果写回到FutureAdapter#result 中,此时则会阻塞当前线程,直至异步调用执行结束,将结果写入 FutureAdapter#result 中。

3. CompletableFuture

在上面我们了解到 FutureAdapter 是 CompletableFuture 的一个子类。在Dubbo异步过程中是用来 CompletableFuture类。CompletableFuture 类的内部逻辑比较复杂,本文不再关注内部实现,仅需要了解其基本使用即可,详参: CompletableFuture基本用法


这里简单提一下:

CompletableFuture 扩展了 Future 接口,在其基础上增加了异步回调、流式处理、Future复合操作等,使Java在处理多任务的协同工作时更加顺畅便利。

CompletableFuture 有两个关键属性:

	// 任务执行结果
    volatile Object result;       // Either the result or boxed AltResult
    // 任务过程中的操作处理
    volatile Completion stack;    // Top of Treiber stack of dependent actions
  • result :保存任务执行的结果
  • stack :对执行结果的操作。

可以简单的理解,当 CompletableFuture.result 被更新后, CompletableFuture.stack 就会基于result 开始执行(stack 有不同的实现,具有不同的功能)。


到这里我们再总结一下:

消费者发起异步请求后会创建一个 DefaultFuture 保存请求信息,可以认为一个 DefaultFuture 对象代表一次异步请求,同时会创建一个FutureAdapter保存DefaultFuture,并设置 DefaultFuture 回调方法,随后将 FutureAdapter 保存到上下文中 。当请求结束,提供者会回调 发DefaultFuture#received 方法,在这个方法中会找到本次请求的 DefaultFuture。更新 DefaultFuture.response 属性并回调设置的回调方法。而DefaultFuture 的回调方法在 FutureAdapter 中,FutureAdapter 回调方法中会更新 自身的result。同时触发 FutureAdapter.stack 的操作。(下面举例中的方式三则是通过这种逻辑来完成的异步调用。)

3.1 CompletableFuture#get

当 CompletableFuture 的异步任务执行结束,会将结果写入到 CompletableFuture#result 中。在此期间调用 CompletableFuture#get 方法则会被阻塞,直至请求结束。

	//  CompletableFuture#get 
    public T get() throws InterruptedException, ExecutionException {
        Object r;
        // 如果 result != null 则返回,否则自旋等待(即阻塞当前线程)
        return reportGet((r = result) == null ? waitingGet(true) : r);
    }

3.2 CompletableFuture#complete

在 CompletableFuture#complete 中 会通过 CompletableFuture#completeValue 方法利用 Unsafe类以 cas 的方式更新 result。


    public boolean complete(T value) {
    	// 1. cas 更新 result 值
        boolean triggered = completeValue(value);
        // 2. 完成后续操作。诸如 thenApply、whenComplete、thenAccept 等操作
        postComplete();
        return triggered;
    }
    
    final boolean internalComplete(Object r) { // CAS from null to r
    	// cas  更新 RESULT,RESULT 保存的是 result 的内存地址
    	// 操作当前对象 (this) 的 result (RESULT 指向的内存地址所保存的属性),如果是 null,就修改为 r。
        return UNSAFE.compareAndSwapObject(this, RESULT, null, r);
    }

当 CompletableFuture 执行一个异步任务时,如果添加了诸如 thenApply、whenComplete、thenAccept 等操作,会被保存到 Completion 中,Completion 是一个抽象类,CompletableFuture 为每个操作实现了一个具体类,同时类似于链表的形式其内部保存下一个 操作的对象。而 CompletableFuture#postComplete方法会依次执行这些操作。

简单来说 CompletableFuture 在完成指定的异步任务后会依次执行后续诸如 thenApply、whenComplete、thenAccept 操作。而 CompletableFuture#complete 方法可以触发 CompletableFuture 来完成这些操作。

三、 异步调用的过程

在介绍完 关键的几个类之后,下面我们开始分析一下异步调用的具体过程。我们这使用Main 方法的方式来分析异步过程。

Dubbo通过参数 async 判断是否异步调用,而 async = true 的情况有如下两种 :

  1. 消费者端可以通过async 来指定是否开启异步调用。
  2. 消费者调用的接口返回类型是 CompletableFuture 也会被判定是 异步调用,该中场景在下面第五部分 CompletableFuture 的异步调用 详解。

接下来的我们进行异步调用的逻辑分析,以下面的代码为例:

	public class SimpleConsumer {
    /**
     * 输出
     *      方式1 同步结果 = null
     *      方式1 异步结果 = sayHello : 方式1
     */
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ReferenceConfig<SimpleSyncDemoService> referenceConfig = DubboUtil.referenceConfig("dubbo-consumer", SimpleSyncDemoService.class);
        referenceConfig.setAsync(true);
        SimpleSyncDemoService demoService = referenceConfig.get();
        // 方式1
        sayHello1(demoService);
        // 方式2
        sayHello2(demoService);
        // 方式3
        sayHello3(demoService);

    }

    /**
     * 方式3  dubbo 2.7.* 提供的异步方式
     * 相较于方式2的改进: API 调用更方便;可以对多个 Future 进行复合操作
     */
    private static void sayHello3(SimpleSyncDemoService demoService) {
        // dubbo 2.7.0 提供
        String result = demoService.sayHello("方式3");
        System.out.println("方式3 同步结果 = " + result);
        RpcContext.getContext().getCompletableFuture()
                .whenComplete(new BiConsumer<Object, Throwable>() {
                    @Override
                    public void accept(Object o, Throwable throwable) {
                        System.out.println("方式3 异步结果 = " + o);
                        if (throwable != null) {
                            System.out.println("throwable = " + throwable);
                        }
                    }
                });
    }

    /**
     * 方式2 dubbo 2.7.0 之前提供的异步方式
     * 设置回调的这种方式不会阻塞业务调用线程,这是借助了Netty的异步通信机制,Netty底层的I/O线程会在接收到响应后自动回调注册的回调函数,不需要业务线程干预。
     */
    private static void sayHello2(SimpleSyncDemoService demoService) {
        String result = demoService.sayHello("方式2");
        System.out.println("方式2 同步结果 = " + result);
        ((FutureAdapter) RpcContext.getContext().getFuture()).getFuture().setCallback(new ResponseCallback() {
            @Override
            public void done(Object response) {
                System.out.println("方式1 异步结果 = " + response);
            }

            @Override
            public void caught(Throwable exception) {
                if (exception != null) {
                    System.out.println("exception = " + exception);
                }
            }
        });
    }

    /**
     * 方式1, dubbo2.7.0 之前提供的异步方式
     * 缺点是 调用 future.get() 会阻塞业务线程。
     */
    private static void sayHello1(SimpleSyncDemoService demoService) throws InterruptedException, ExecutionException {
        String result = demoService.sayHello("方式1");
        System.out.println("方式1 同步结果 = " + result);
        Future<Object> future = RpcContext.getContext().getFuture();
        System.out.println("方式1 异步结果 = " + future.get());
    }

}

上面的代码有三种异步调用方式,其实现的功能都是异步调用,区别在于:

  • 方式一 :调用 RpcContext.getContext().getFuture().get() 获取到异步结果时会阻塞用户线程,这不能算一个真正的异步调用。
  • 方式二 :方式二提供了回调方法解决了方式一的问题,但是API 的调用不够友好,并且无法对结果进行复合处理。
  • 方式三:Api 更加友好,并且基于CompletableFuture 的特性可以对结果做复合处理。

需要注意的是 :对于一次异步调用,一般情况下有两个返回结果,一个是同步返回结果,即调用后立刻返回的结果集,一般没有意义。另一个是异步返回结果,即提供者真正处理完请求后返回的结果集。


为了更好的理解调用过程,我们先以上面代码中的方式一为例进行代码分析

我们这里按照顺序分析:

  1. 消费者端发起异步调用的流程
  2. 消费者端接收异步结果的流程

1. 异步调用的发起

我们通过 demoService.sayHello("方式1"); 发起异步调用时,根据我们之前讲过的流程,会执行到 DubboInvoker#doInvoke 方法中,异步调用的发起就是在这完成。

调用时序图如下:图源《深度剖析Apache Dubbo 核心技术内幕》


我们这里来看一下 DubboInvoker#doInvoke 的实现:

    @Override
    protected Result doInvoke(final Invocation invocation) throws Throwable {
        RpcInvocation inv = (RpcInvocation) invocation;
       	// ... 其他处理
        try {
        	// 是否异步,判断你条件是 RpcInvocation 中的 async 属性
            boolean isAsync = RpcUtils.isAsync(getUrl(), invocation);
            // 是否异步 Future,判断条件是 RpcInvocation 中的 future_returntype 属性
            boolean isAsyncFuture = RpcUtils.isReturnTypeFuture(inv);
            // 是否不需要返回响应
            boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
            int timeout = getUrl().getMethodParameter(methodName, Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT);
            // 同步 && 不需要返回值
            if (isOneway) {
                boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);
                // 发起同步请求
                currentClient.send(inv, isSent);
                // 设置 上下文 Future为空
                RpcContext.getContext().setFuture(null);
                return new RpcResult();
            } else if (isAsync) {
            	/******* 异步调用 *********/
            	// 1. 进行异步调用
                ResponseFuture future = currentClient.request(inv, timeout);
                // For compatibility
                // 2. 使用 FutureAdapter 包装 future
                FutureAdapter<Object> futureAdapter = new FutureAdapter<>(future);
                // 将适配器设置到上下文中。
                RpcContext.getContext().setFuture(futureAdapter);

                Result result;
                // 3. 返回结果的处理。如果方法返回类型是CompletableFuture,将结果包装成 AsyncRpcResult 返回。否则返回SimpleAsyncRpcResult
                if (isAsyncFuture) {
                    // register resultCallback, sometimes we need the async result being processed by the filter chain.
                    // 3.1 如果异步请求需要返回类型是 CompletableFuture ,则将结果包装成 AsyncRpcResult
                    result = new AsyncRpcResult(futureAdapter, futureAdapter.getResultFuture(), false);
                } else {
                	// 3.2 如果异步请求返回类型不是  CompletableFuture ,则将结果包装成 SimpleAsyncRpcResult
                    result = new SimpleAsyncRpcResult(futureAdapter, futureAdapter.getResultFuture(), false);
                }
                return result;
                /**********************/
            } else {
            	// 其他情况,同步需要返回值
                RpcContext.getContext().setFuture(null);
                // 直接阻塞线程异步调用结果,这里调用的是 DefaultFuture#get
                return (Result) currentClient.request(inv, timeout).get();
            }
        }
        ....
    }

我们这里只关注异步调用的过程即 isAsync 为true 的流程,对于异步请求的处理大概可以分成下面过程:

  1. 发起异步调用:异步请求直接执行 currentClient.request(inv, timeout); 进行异步远程调用,该调用并不会阻塞,而是会马上返回一个 ResponseFuture 对象。需要注意的是,如果需要发起同步远程调用,则是调用 currentClient.send(inv, isSent);。这里的currentClient 是 ReferenceCountExchangeClient 类型。
  2. 使用 FutureAdapter 包装 future,并保存到上下文中:也就是说我们代码中通过 RpcContext.getContext().getFuture(); 获取的 future 即是此时的 FutureAdapter
  3. 对返回结果的处理:如果方法返回类型是CompletableFuture,将结果包装成 AsyncRpcResult 返回。否则返回SimpleAsyncRpcResult这里特别注意 FutureAdapter 作为 AsyncRpcResult 的构造函数入参,被赋值给了 AsyncRpcResult#valueFuture,所以这里 FutureAdapter 对象 等同于 AsyncRpcResult#valueFuture,同理FutureAdapter#resultFuture 等同于AsyncRpcResult#resultFuture

其中第一步发起了异步调用,第二步和第三步都是为了获取异步调用的结果。下面我们一步一步来看:

1.1 发起调用

这里我们来看第一步中的 currentClient.request(inv, timeout); 。 currentClient 是 类型 ReferenceCountExchangeClient。所以这里是通过 ReferenceCountExchangeClient#request 方法发起了一个异步请求调用。


如下时序图 ReferenceCountExchangeClient#request 会委托给 HeaderExchangeChannel#request 来处理请求。
在这里插入图片描述


因此 HeaderExchangeChannel#request 才是我们需要分析的内容,其实现如下:

   @Override
    public ResponseFuture request(Object request) throws RemotingException {
        return request(request, channel.getUrl().getPositiveParameter(Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT));
    }

    @Override
    public ResponseFuture request(Object request, int timeout) throws RemotingException {
        if (closed) {
            throw new RemotingException(this.getLocalAddress(), null, "Failed to send request " + request + ", cause: The channel " + this + " is closed!");
        }
        // create request.
        // 1. 创建 Request 对象
        Request req = new Request();
        req.setVersion(Version.getProtocolVersion());
        // 设置需要返回值
        req.setTwoWay(true);
        // 设置请求数据
        req.setData(request);
        // 2. 创建 DefaultFuture 对象
        DefaultFuture future = DefaultFuture.newFuture(channel, req, timeout);
        try {
        	// 3. 请求发送到远端
            channel.send(req);
        } catch (RemotingException e) {
        	// 出现异常,取消调用。
            future.cancel();
            throw e;
        }
        // 返回 future。为 DefaultFuture 
        return future;
    }

上面代码的逻辑并不复杂,代码1创建了请求对象,然后代码2创建了一个future对象,代码3使用底层通信异步发送请求(使用Netty的I/O线程把请求写入远端),因此代码3是非阻塞的,所以会马上返回。


需要注意的是:

  1. 从用户线程发起远程调用到返回request,使用的都是用户线程。由于代码3 channel.send(req)会马上返回,所以不会阻塞用户线程。

  2. 代码2 中调用了 DefaultFuture#newFuture 来创建 DefaultFuture。关于 DefaultFuture#newFuture 我们在上面的关键类中讲过,DefaultFuture.newFuture 中创建了一个 DefaultFuture 并添加了一个超时定时器,在调用超时后会触发。

1.2 Future 的包装

这里使用FutureAdapter 包装future,并设置到了 Dubbo 上下文中。我们方式一获取调用结果时 RpcContext.getContext().getFuture(); 获取的Future 就是此刻的设置的 FutureAdapter。需要注意这里的 future 是 currentClient.request(inv, timeout); 返回的 DefaultFuture。

  // 2. 使用 FutureAdapter 包装 future
  FutureAdapter<Object> futureAdapter = new FutureAdapter<>(future);
  // 将适配器设置到上下文中。
  RpcContext.getContext().setFuture(futureAdapter);

注意:

  1. 在上面关键类中我们讲过 FutureAdapter的构造函数时提到 FutureAdapter 的构造函数在初始化时会给 future 添加回调方法,该回调方法是完成 FutureAdapter.result 更新的关键。
  2. 这里将 FutureAdapter 设置到了 上下文中。即是说我们通过 RpcContext 获取到的 Future 实际上就是这里的 FutureAdapter 。

1.3 返回结果处理

当消费者发起异步调用后,需要立即返回一个同步结果。上面我们提到,无论是异步调用还是同步调用,同步返回结果 value 都是 RpcResult#recreate方法的返回值。

		// isAsyncFuture 判断条件是 url 中的 future_returntype 为true
       if (isAsyncFuture) {
           // register resultCallback, sometimes we need the async result being processed by the filter chain.
           result = new AsyncRpcResult(futureAdapter, futureAdapter.getResultFuture(), false);
       } else {
           result = new SimpleAsyncRpcResult(futureAdapter, futureAdapter.getResultFuture(), false);
       }

如上代码:这里对于结果返回,这里分为两种情况:

  1. 如果调用的方法的返回类型为 CompletableFuture ,则将结果包装成 AsyncRpcResult 返回。 Dubbo 异步调用接口可以声明返回为类型为 CompletableFuture。对于如下。这种情况调用结果返回结果为 AsyncRpcResult 。这种情况我们在下面 第五部分 CompletableFuture 的异步调用 部分进行了分析。

        CompletableFuture<String> sayHello2(String msg);
    
  2. 反之则结果包装成 SimpleAsyncRpcResult 返回。而AsyncRpcResult 和 SimpleAsyncRpcResult 的区别在于 AsyncRpcResult#recreate 会直接将 valueFuture 返回,而 SimpleAsyncRpcResult#recreate 则会返回 null。

而对于本文中的方式一,我们调用的 SimpleSyncDemoService#sayHello 返回类型是 String,所以这里返回的同步结果类型是 SimpleAsyncRpcResult。而在分析FutureAdapter 构造函数 中我们提到,消费者最终获取到的结果是 Result#recreate() 方法返回的结果。 也就是说 RpcResult#recreate 才是最终同步调用返回的结果值。


这里对于方式一的异步调用应该就结束了。其简略时序图如下:

在这里插入图片描述


到这里,对于方式一的异步调用发起流程已经分析结束。

2. 异步结果的获取

在上面,我们分析了整个异步调用发起以及同步结果返回的过程,现在我们来分析一下异步调用结果如何获取的。在方式一的代码中,我们可以通过 RpcContext.getContext().getFuture().get() 来获取异步调用的结果。

我们在上面提到过 RpcContext.getContext().getFuture() 获取的值是在HeaderExchangeChannel#request 中 创建的 FutureAdapter

所以这里的 RpcContext.getContext().getFuture().get(),即 FutureAdapter#get方法,其实现如下:

    @Override
    @SuppressWarnings("unchecked")
    public V get() throws InterruptedException, ExecutionException {
        try {
            return super.get();
        } catch (ExecutionException | InterruptedException e) {
            throw e;
        } catch (Throwable e) {
            throw new RpcException(e);
        }
    }

这里的 super.get(); 调用的是 CompletableFuture#get()CompletableFuture#get()会验证 result 是否为空,如果不为空则返回,否则阻塞线程等待。

    public T get() throws InterruptedException, ExecutionException {
        Object r;
        // 如果 result 为空,则调用waitingGet(true) 方法等待。否则返回结果,供reportGet 方法校验转换后返回。
        return reportGet((r = result) == null ? waitingGet(true) : r);
    }

现在我们知道了调用 RpcContext.getContext().getFuture().get() 会阻塞当前线程直到 FutureAdapter#result 不为空(result 是 FutureAdapter 父类CompletableFuture 的属性)。我们这里可以认为 当异步请求结束后会将请求结果写回到 FutureAdapter#result 属性中。那么我们此时就需要来分析一下 result 是如何更新的。


我们在 1.3 DefaultFuture#received 中讲到过当异步请求结束后,会调用 DefaultFuture#received。其调用流程如下:

当提供者将请求结果发送给消费者时,消费者会触发
	HeaderExchangeHandler#received 
	  ->  HeaderExchangeHandler#handleResponse 
			->  DefaultFuture.received 

DefaultFuture#received 方法的逻辑我们在上面的关键类已经讲过。DefaultFuture#received 中会调用其回调方法。而 FutureAdapter 在构造函数中已经为本次请求的 DefaultFuture设置了回调方法。即是说当异步请求结束后会回调 FutureAdapter 内部的 DefaultFuture 的回调方法。其实现如下:

  		future.setCallback(new ResponseCallback() {
            @Override
            public void done(Object response) {
                Result result = (Result) response;
                // 调用了 CompletableFuture#complete。触发Dubbo Filter 链的操作
                FutureAdapter.this.resultFuture.complete(result);
                V value = null;
                try {
                	// 调用 recreate 方法最后一次构建结果值。
                    value = (V) result.recreate();
                } catch (Throwable t) {
                    FutureAdapter.this.completeExceptionally(t);
                }
                 // 调用了 CompletableFuture#complete。触发了 FutureAdapter 的后续操作。
                 // 在这里将 value 写入到了 当前 FutureAdapter#result 中。从而激活了RpcContext.getContext().getFuture().get()
                FutureAdapter.this.complete(value);
            }

            @Override
            public void caught(Throwable exception) {
                FutureAdapter.this.completeExceptionally(exception);
            }
        });

其中 FutureAdapter.this.complete(value); 则会通过 cas 更新 result 的值(也就是说异步调用返回的结果会被写到FutureAdapter.result 属性中),来激活 RpcContext.getContext().getFuture().get() 的获取。

  1. 当请求结束后,提供者将结果返回后,消费者会触发 HeaderExchangeHandler#received 方法。消费者根据 HeaderExchangeHandler#received -> HeaderExchangeHandler#handleResponse -> DefaultFuture#received 流程调用到 DefaultFuture#received 方法 。
  2. 在 DefaultFuture#received 中会根据返回信息中的request id找到对应这次请求的 DefaultFuture 赋值 response 属性并触发回调方法。
  3. DefaultFuture的回调方法在 FutureAdapter 中声明,会将请求结果赋值给 FutureAdapter#result,并触发 FutureAdapter#的后续操作。在关键类中 FutureAdapter 构造函数的描述中我们讲了这部分内容
  4. FutureAdapter#result被更新后, RpcContext.getContext().getFuture().get() 则会被激活。至此完成异步调用结果的获取。

3. 总结

我们这里总结一下方式一情况下的异步调用流程:

  1. 消费者通过 demoService.sayHello("方式1"); 方式发起了异步调用,程序执行到 DubboInvoker#doInvoke 中的 currentClient.request(inv, timeout) 发起异步调用,创建 DefaultFuture 并返回。
  2. 随后在DubboInvoker#doInvoke 方法中 创建FutureAdapter 时 会对 DefaultFuture 进行包装,并将FutureAdapter 设置到 Rpc 上下文中,注每意一次请求对应一个唯一的FutureAdapter 对象和 DefaultFuture 对象。在此时FutureAdapter 会 给 DefaultFuture 设置结果回调(详参 关键类中 FutureAdapter 构造函数的描述),当请求结束时会回调该回调方法。
  3. 随后返回同步调用结果(AsyncRpcResult 或 SimpleAsyncRpcResult)。我们这里返回的类型是 SimpleAsyncRpcResult
  4. InvokerInvocationHandler#invoke 中会调用 SimpleAsyncRpcResult#recreate 方法获取到同步返回结果值,而 SimpleAsyncRpcResult#recreate 返回为 null。所以同步调用结果为 null。
  5. 随后我们在主线程中通过 RpcContext.getContext().getFuture().get() 来获取异步调用结果,此时如果异步请求结束了则会直接返回,否则会阻塞当前线程。假设当前异步请求还未完成,则当前线程阻塞等待。请求是否完成的判断依据在于 FutureAdapter#result 是否为空。
  6. 当提供者处理完异步调用后,将结果返回,会触发消费者 HeaderExchangeHandler#received 方法,该方法会调用 DefaultFuture#receivedDefaultFuture#response会保存请求返回值,并且如果 DefaultFuture 设置了回调方法,则会执行回调方法。而我们上面说过 FutureAdapter 会 给 DefaultFuture 设置回调方法,所以此时会执行到第二步中说的回调方法。该回调方法会将结果通过 CAS 的方式写入到 FutureAdapter#result中(result 是 FutureAdapter 父类 CompletableFuture 的属性),并触发FutureAdapter 的后续操作。
  7. 此时 FutureAdapter#result 被更新,则 RpcContext.getContext().getFuture().get() 不再阻塞,将异步结果返回。此时整个调用结果结束。

四、方式二、三的异步调用

有了方式一的逻辑作为基础,方式二、三都是 方式一的变种,核心思路相同。

1 方式二

在方式一种,FutureAdapter#future 设置了回调,当异步调用结果返回时会触发该回调,随后将修改 FutureAdapter#result。 RpcContext.getContext().getFuture().get() 感知到 result 修改,不再阻塞,而是将 result 返回。
即流程可以总结如下:

请求结束,结果写回
 -> FutureAdapter#future 回调触发 
	-> FutureAdapter#result 更新 
		->  用户线程调用 RpcContext.getContext().getFuture().get() 感知result,返回结果

而方式二则是直接修改了FutureAdapter#future 的回调。因为 FutureAdapter#future 的回调本身就是为了通知 FutureAdapter#result 更新,让RpcContext.getContext().getFuture().get() 感知。也即是说 FutureAdapter#future 回调触发时即是异步调用结果返回。

即流程可以总结如下:

请求结束,结果写回
 -> FutureAdapter#future 回调触发 (用户线程直接重写该回调方法)

代码实现如下:

    /**
     * 方式2 dubbo 2.7.0 之前提供的异步方式
     * 设置回调的这种方式不会阻塞业务调用线程,这是借助了Netty的异步通信机制,Netty底层的I/O线程会在接收到响应后自动回调注册的回调函数,不需要业务线程干预。
     */
    private static void sayHello2(SimpleSyncDemoService demoService) {
        String result = demoService.sayHello("方式2");
        System.out.println("方式2 同步结果 = " + result);
        // ((FutureAdapter) RpcContext.getContext().getFuture()).getFuture() 即获取到了FutureAdapter中的 future。重写设置其回调方法。 
        ((FutureAdapter) RpcContext.getContext().getFuture()).getFuture().setCallback(new ResponseCallback() {
            @Override
            public void done(Object response) {
                System.out.println("方式1 异步结果 = " + response);
            }

            @Override
            public void caught(Throwable exception) {
                if (exception != null) {
                    System.out.println("exception = " + exception);
                }
            }
        });
    }

2 方式三

方式三则是直接利用了CompletableFuture 的特性。当异步任务执行结束后会调用FutureAdapter#future 的回调方法,此时会通过 FutureAdapter.this.complete(value); 方法更新result 值并触发FutureAdapter 的后续操作。我们可以通过 FutureAdapter#whenComplete 方法感知到任务结束,借此来处理结果。

    /**
     * 方式3  dubbo 2.7.* 提供的异步方式
     * 相较于方式2的改进: API 调用更方便;可以对多个 Future 进行复合操作
     */
    private static void sayHello3(SimpleSyncDemoService demoService) {
        // dubbo 2.7.0 提供
        String result = demoService.sayHello("方式3");
        System.out.println("方式3 同步结果 = " + result);
        // RpcContext.getContext().getCompletableFuture() 即是 强转成 CompletableFuture类型的 FutureAdapter
        RpcContext.getContext().getCompletableFuture()
                .whenComplete(new BiConsumer<Object, Throwable>() {
                    @Override
                    public void accept(Object o, Throwable throwable) {
                        System.out.println("方式3 异步结果 = " + o);
                        if (throwable != null) {
                            System.out.println("throwable = " + throwable);
                        }
                    }
                });
    }

五、CompletableFuture 的异步调用

上面我们提到消费者调用的接口返回类型是 CompletableFuture 也会被判定是 异步调用。

原因在于:当消费端发起远程调用时,请求会被InvokerInvocationHandler拦截,并在其中创建RpcInvocation,并且如果调用方法的返回值为CompletableFuture或者其子类,则会把future_returntype为true和async=true的属性添加到RpcInvocation的附加属性Map中。而在 AbstractProxyInvoker#invoke 中会根据 future_returntype 参数判断是否是 CompletableFuture 请求,根据async判断是否是异步请求。

InvokerInvocationHandler#createInvocation代码如下:

	// InvokerInvocationHandler#createInvocation
    private RpcInvocation createInvocation(Method method, Object[] args) {
        RpcInvocation invocation = new RpcInvocation(method, args);
        // 如果返回类型为 CompletableFuture或者其子类。则认为是异步调用。设置 future_returntype为true和async=true
        if (RpcUtils.hasFutureReturnType(method)) {
            invocation.setAttachment(Constants.FUTURE_RETURNTYPE_KEY, "true");
            invocation.setAttachment(Constants.ASYNC_KEY, "true");
        }
        return invocation;
    }

返回类型是 CompletableFuture 的方法一般都用于提供者的异步执行。关于异步执行的内容,我们而在 Dubbo笔记 ㉑ :提供者的异步执行 中进行介绍。我们这里只介绍消费者端如何处理这种情况。

对于 CompletableFuture 返回类型,消费者端最大的差别在于 DubboInvoker#doInvoke 方法中,我们看到如果方法返回类型是 CompletableFuture ,则在 DubboInvoker#doInvoke 中返回 AsyncRpcResult ,否则返回SimpleAsyncRpcResult。如下:在这里插入图片描述

而AsyncRpcResult 和 SimpleAsyncRpcResult 的区别在于 AsyncRpcResult#recreate 会直接将 valueFuture 返回,而 SimpleAsyncRpcResult#recreate 则会返回 null。

所以综上,

  • CompletableFuture 返回类型是的方法异步调用,获取的同步结果是 AsyncRpcResult#recreate 返回值,即 AsyncRpcResult#valueFuture
  • 非CompletableFuture 返回类型的方法异步调用,获取的同步结果是 SimpleAsyncRpcResult#recreate 返回值,即 null。

我们以消费者端下面代码为例:

	// 1. 调用返回类型是 CompletableFuture 的方法,此时的返回值completableFuture 即为上下文中  
	// completableFuture  等价于 RpcContext.getContext().getCompletableFuture()
   CompletableFuture<String> completableFuture = demoService.sayHello2("sayHello2");
   // 获取上下文中的 contextCompletableFuture 
   CompletableFuture<String> contextCompletableFuture = RpcContext.getContext().getCompletableFuture();
   // 2. 这里返回为 true
   System.out.println(completableFuture == contextCompletableFuture);
   // 3. 等异步调用结束后打印结果
   completableFuture.whenComplete((result, throwable) -> System.out.println("result = " + result));
  1. 在 DubboInvoker#doInvoke 方法中我们可以得知 AsyncRpcResult#valueFuture 就是我们上面提到的 Dubbo上下文中的 FutureAdapter。

    在这里插入图片描述

  2. 所以这里completableFuture == contextCompletableFuture 返回为 true。

  3. 而根据上面三种方式的讲解,我们便可以通过代码三的方式来等待异步请求结束后再处理请求结果。所以这里的代码类似于上面讲过的方式三的逻辑。


综上我们得知:
对于 CompletableFuture 返回类型 的方法,Dubbo默认通过异步调用方式,并且可以通过其同步返回的 CompletableFuture 来处理结果。另外需要注意的是,消费者端在构造AsyncRpcResult 和 SimpleAsyncRpcResult 时最后一个参数传的都是false,该参数是为了解决提供者端异步调用的Filter 链问题,详参衍生篇内容 Dubbo衍生篇⑦ :异步场景下的问题


以上:内容部分参考
《深度剖析Apache Dubbo 核心技术内幕》
https://dubbo.apache.org/zh/docs/v2.7/dev/source/
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Dubbo是一个高性能的开源RPC框架,用于构建分布式服务架构。要调用Dubbo服务,首先需要做以下几个步骤: 1. 引入Dubbo依赖:在你的项目中,需要引入Dubbo的相关依赖。你可以在Maven等构建工具中添加Dubbo的依赖,例如: ```xml <dependency> <groupId>com.alibaba</groupId> <artifactId>dubbo</artifactId> <version>x.x.x</version> </dependency> ``` 请确保将`x.x.x`替换为你要使用的Dubbo版本。 2. 配置Dubbo消费者:在你的项目配置文件中,需要配置Dubbo消费者的相关信息。这包括注册中心的地址、服务接口、负载均衡策略等。一个示例的Dubbo消费者配置如下: ```xml <dubbo:application name="consumer-app" /> <dubbo:registry address="zookeeper://localhost:2181" /> <dubbo:reference id="userService" interface="com.example.UserService" /> <dubbo:consumer check="false" /> ``` 在上面的示例中,我们指定了注册中心地址为本地ZooKeeper服务,并引用了一个名为`userService`的服务。 3. 调用Dubbo服务:在代码中,你可以通过获取`userService`的实例来调用Dubbo服务方法。你可以通过Spring的依赖注入或者通过Dubbo的API手动获取实例。 ```java @Service public class UserServiceConsumer { @Autowired private UserService userService; public void doSomething() { // 调用Dubbo服务方法 String result = userService.getUserInfo(123); System.out.println(result); } } ``` 在上面的示例中,我们通过自动注入获取了`userService`的实例,并调用了`getUserInfo`方法。 以上是一个简单的Dubbo消费者的调用过程。需要注意的是,你需要确保Dubbo提供者已经注册到相应的注册中心,并且提供者和消费者的接口、版本号等必须一致。此外,你还可以根据实际情况自定义Dubbo的配置和扩展。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猫吻鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值