12、Dubbo源码系列-异步请求原理解析

609 阅读4分钟

人生如逆旅,我亦是行人。前文为大家分享了Dubbo的线程模型以及线程池支持,趁热打铁,今天就继续和大家一起分享下Dubbo的同步、异步请求原理。

一、异步请求demo

public class AsyncConsumerApplication {

    public static void main(String[] args) {
        ReferenceConfig<DemoService> reference = new ReferenceConfig<>();
        reference.setInterface(DemoService.class);
        // 开始异步请求
        reference.setAsync(true);
        reference.setTimeout(5000);
        DubboBootstrap bootstrap = DubboBootstrap.getInstance();
        bootstrap
                .application(new ApplicationConfig("dubbo-demo-api-consumer"))
                .registry(new RegistryConfig("zookeeper://127.0.0.1:2181"))
                .reference(reference)
                .start();

        // 发起rpc调用,此时获取到的返回值为null
        String message = ReferenceConfigCache.getCache().get(reference).sayHello("dubbo");

        System.out.println("result::" + message);

        CompletableFuture<Object> future = RpcContext.getContext().getCompletableFuture();

        future.whenComplete((v, t) -> {
            if (t != null) {
                t.printStackTrace();
            } else {
                // 异步请求完成后,则会打印"Hello dubbo"
                System.out.println("async result::" + v);
            }
        });

    }
}

为了讲解方便,依然使用API发起异步请求,首先通过reference.setAsync(true)设置开启异步请求,通过ReferenceConfigCache正常的发起RPC请求,此时获取到的结果message为空。通过RpcContext获取到异步结果对应的CompletableFuture,当future complete后,打印返回值。看到这,大家应该都很熟悉了,没错,就是java8的CompletableFuture,支持很多特性,这里就不展示讲了,值得注意的是,Dubbo其实是在2.7版本以上才支持CompletableFuture,本文基于2.7.5版本。

二、异步调用流程分析

2.1. RpcContext.getContext().getCompletableFuture()

查看RpcContext.getContext().getCompletableFuture()实现如下:

    public <T> CompletableFuture<T> getCompletableFuture() {
        return FutureContext.getContext().getCompletableFuture();
    }

可以看到内部是通过FutureContext.getContext().getCompletableFuture()方法获取到对应的异步future,debug如下: image.png 这里的future类型为FutureAdapter

2.2. FutureAdapter

查看FutureAdapter实现如下:

    public FutureAdapter(CompletableFuture<AppResponse> future) {
        this.appResponseFuture = future;
        future.whenComplete((appResponse, t) -> {
            if (t != null) {
                if (t instanceof CompletionException) {
                    t = t.getCause();
                }
                this.completeExceptionally(t);
            } else {
                if (appResponse.hasException()) {
                    this.completeExceptionally(appResponse.getException());
                } else {
                    this.complete((V) appResponse.getValue());
                }
            }
        });
    }

可以看到其实没做什么工作,只是对future进行了一层封装,查看FutureAdapter调用链可以发现,是在AbstractInvoker invoke中被调用的。

2.3. AbstractInvoker-invoke

 public Result invoke(Invocation inv) throws RpcException {
       
        AsyncRpcResult asyncResult= (AsyncRpcResult) doInvoke(invocation);
        RpcContext.getContext().setFuture(new FutureAdapter(asyncResult.getResponseFuture()));
        return asyncResult;
    }

    protected abstract Result doInvoke(Invocation invocation) throws Throwable;

可以看到,invoke方法中,先调用抽象方法doInvoke获取到result,然后对Result中的ResponseFuture进行封装(说实话这里没看太懂,为啥不直接返回ResponseFuture呢),查看doInvke实现如下: image.png 又看到熟悉的面孔了,DubboInvker,前文讲解客户端调用流程时曾提到过,主要用来进行服务调用的,下面就让我们一起再回顾下其doInvoke方法

2.4. DubboInvker-doInvoke

    @Override
    protected Result doInvoke(final Invocation invocation) throws Throwable {
            ... ... 
            boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
            int timeout = getUrl().getMethodPositiveParameter(methodName, TIMEOUT_KEY, DEFAULT_TIMEOUT);
            if (isOneway) {
                boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);
                currentClient.send(inv, isSent);
                return AsyncRpcResult.newDefaultAsyncResult(invocation);
            } else {
                ExecutorService executor = getCallbackExecutor(getUrl(), inv);
                CompletableFuture<AppResponse> appResponseFuture =
                        currentClient.request(inv, timeout, executor).thenApply(obj -> (AppResponse) obj);
                FutureContext.getContext().setCompatibleFuture(appResponseFuture);
                AsyncRpcResult result = new AsyncRpcResult(appResponseFuture, inv);
                result.setExecutor(executor);
                return result;
            }
    }
  • 首先通过isOneway参数用来做逻辑判断,这个参数用来判断需要不需要回调,即是否是单向请求。
  • 然后通过getCallbackExecutor方法获取执行请求的线程池。
  • 最后把请求响应结果交给放入AsyncRpcResult中。

看到这不知道大家有没有一个疑惑,为什么没看到判断当前RPC请求是否是异步的代码呢?难道Dubbo所有的请求都是异步的?当然不是。

2.5. getCallbackExecutor

    protected ExecutorService getCallbackExecutor(URL url, Invocation inv) {
        ExecutorService sharedExecutor = ExtensionLoader.getExtensionLoader(ExecutorRepository.class).getDefaultExtension().getExecutor(url);
        if (InvokeMode.SYNC == RpcUtils.getInvokeMode(getUrl(), inv)) {
            return new ThreadlessExecutor(sharedExecutor);
        } else {
            return sharedExecutor;
        }
    }

当前如果是同步请求,使用ThreadlessExecutor线程池,否则使用sharedExecutor。

2.6. ThreadlessExecutor

关于ThreadlessExecutor有必要介绍下它的特点,官方文档说明如下:

  • 和其他的线程池最大的区别是它不管理任何线程。
  • 提交给它的任务不会由单独的线程去调度执行。
  • 被存放在阻塞队列中的任务只有被调用了waitAndDrain()才会执行,并且执行任务的线程是当前调用了waitAndDrain()方法的线程。

什么意思呢?也就是说,哪个线程来调用AsyncRpcResult get方法获取RPC调用结果,就由哪个线程来执行线程池的里的任务。查看AsyncRpcResult get实现如下:

    @Override
    public Result get() throws InterruptedException, ExecutionException {
        if (executor != null && executor instanceof ThreadlessExecutor) {
            ThreadlessExecutor threadlessExecutor = (ThreadlessExecutor) executor;
            threadlessExecutor.waitAndDrain();
        }
        return responseFuture.get();
    }

可以看到,当前线程池类型如果是ThreadlessExecutor时候,调用threadlessExecutor.waitAndDrain()来获取RPC调用结果,否则调用 responseFuture.get()获取异步请求结果。

三、小结

本篇文章主要为大家分析了Dubbo 2.7.5版本异步RCP的实现流程,同时顺带着讲解下了同步调用的原理,由于Dubbo底层采用Netty通信,本质上都是异步的I/O请求,是不会有同步调用的实现。Dubbo内部借助了ThreadlessExecutor线程池,实现了同步阻塞调用(其实也可以换成其他阻塞队列)。其他RPC不知道大家没有没了解过是怎么实现同步调用的呢?欢迎评论区讨论分享。