基于dubbo实现异步调用(1)

1.前言

Java中常见的实现异步调用的方式:
1.ThreadPool
2.CompletableFuture
3.MQ
4.BlockingQueue
5.Fork/Join

那么作为一款优秀的RPC框架,dubbo是如何实现异步调用的呢?本文将介绍2.6.x版本以来dubbo异步调用方式的演进。
1.增加consumer配置
2.参数回调(2.7.0已废弃,本文将不展开)
3.事件通知
4.直接定义返回CompletableFuture的服务接口
5.利用AsyncFor注解实现客户端的同步转异步
6.利用RpcContext.startAsync()实现服务端的同步转异步
其中前面3种方式在2.6.x版本中就已支持,但参数回调在2.7.0版本中已废弃,后面3种则是在2.7.0版本中新增的方式。

2.基于dubbo实现异步调用

2.1 增加consumer配置

这种方式很简单,只需要在服务引用时增加<dubbo:method>配置即可,如下所示,其中name为需要异步调用的方法名,async表示是否启用异步调用。

<dubbo:reference id="asyncService" check="false" interface="com.alibaba.dubbo.demo.AsyncService" url="localhost:20880">
    <dubbo:method name="sayHello" async="true" />
</dubbo:reference>

此时consumer端有3种调用方式:

  • 由于配置了异步调用,因此此时直接调用将返回null:
String result = asyncService.sayHello("world");
  • 通过RpcContext获取Future对象,调用get方法时阻塞知道返回结果:
asyncService.sayHello("world");
Future<String> future = RpcContext.getContext().getFuture();
String result = future.get();
  • 通过ResponseFuture设置回调,执行完成会回调done方法,抛异常则会回调caught方法:
asyncService.sayHello("world");
ResponseFuture responseFuture = ((FutureAdapter)RpcContext.getContext().getFuture()).getFuture();
responseFuture.setCallback(new ResponseCallback() {
    @Override
    public void done(Object response) {
        System.out.println("done");
    }

    @Override
    public void caught(Throwable exception) {
        System.out.println("caught");
    }
});

try {
    System.out.println("result = " + responseFuture.get());
} catch (RemotingException e) {
    e.printStackTrace();
}

如果只想异步调用,不需要返回值,则可以配置 return="false",这样可以避免Future对象的创建,此时RpcContext.getContext().getFuture()将返回null;

2.2 直接定义返回CompletableFuture的服务接口

在上述方式中,想获取异步调用的结果,需要从RpcContext中获取,使用起来不是很方便。基于java 8中引入的CompletableFuture,dubbo在2.7.0版本中也增加了对CompletableFuture的支持,我们可以直接定义一个返回CompletableFuture类型的接口。

public interface AsyncService {

    String sayHello(String name);

    CompletableFuture<String> sayHelloAsync(String name);
}

服务端实现如下:

public class AsyncServiceImpl implements AsyncService {

    @Override
    public String sayHello(String name) {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return name;
    }


    @Override
    public CompletableFuture<String> sayHelloAsync(String name) {
        return CompletableFuture.supplyAsync(() -> name);
    }
}

如此一来,我们就实现了服务端的异步,客户端直接调用接口即可,不需要再从RpcContext中获取返回值:

CompletableFuture<String> completableFuture = asyncService.sayHelloAsync("async");
String result = completableFuture.get();
2.3 事件通知

dubbo允许consumer 端在调用之前、调用之后或出现异常时,触发 oninvoke、onreturn、onthrow 三个事件。类似于Spring中的前置增强、后置增强和异常抛出增强。只需要在服务引用时,增加以下配置指定事件通知的方法即可:

<dubbo:reference id="asyncService" check="false" interface="com.alibaba.dubbo.demo.AsyncService" url="localhost:20880">
    <dubbo:method name="sayHello" 
                  oninvoke="notifyServiceImpl.onInvoke" 
                  onreturn="notifyServiceImpl.onReturn" 
                  onthrow="notifyServiceImpl.onThrow" />
</dubbo:reference>

事件通知服务如下:

public class NotifyServiceImpl implements NotifyService {

    // 方法参数与调用方法参数相同
    @Override
    public void onInvoke(String name) {
        System.out.println("onInvoke: " + name);
    }

    // 第一个参数为调用方法的返回值,其余为调用方法的参数
    @Override
    public void onReturn(String retName, String name) {
        System.out.println("onReturn: " + name);
    }

    // 第一个参数为调用异常,其余为调用方法的参数
    @Override
    public void onThrow(Throwable ex, String name) {
        System.out.println("onThrow: " + name);
    }
}

与Spring增强不同的是,dubbo中的事件通知也可以是异步,只需要将调用方法配置为async="true"即可,但oninvoke方法无法异步执行。

2.4 异步调用源码分析

dubbo中的异步调用实际上是通过引入一个FutureFilter来实现的,关键源码如下。

2.4.1 调用前获取方法信息
@Activate(group = Constants.CONSUMER)
public class FutureFilter implements PostProcessFilter {

    protected static final Logger logger = LoggerFactory.getLogger(FutureFilter.class);

    @Override
    public Result invoke(final Invoker<?> invoker, final Invocation invocation) throws RpcException {
        fireInvokeCallback(invoker, invocation);
        // need to configure if there's return value before the invocation in order to help invoker to judge if it's
        // necessary to return future.
        return postProcessResult(invoker.invoke(invocation), invoker, invocation);
    }
    ...
}

在fireInvokeCallback()方法中,会首先调用getAsyncMethodInfo()获取目标方法的方法信息,看是否有配置事件通知:

private ConsumerMethodModel.AsyncMethodInfo getAsyncMethodInfo(Invoker<?> invoker, Invocation invocation) {
    // 首先获取消费者信息
    final ConsumerModel consumerModel = ApplicationModel.getConsumerModel(invoker.getUrl().getServiceKey());
    if (consumerModel == null) {
        return null;
    }

    // 获取消费者对应的方法信息
    ConsumerMethodModel methodModel = consumerModel.getMethodModel(invocation.getMethodName());
    if (methodModel == null) {
        return null;
    }

    // 获取消费者对应方法的事件信息,即是否有配置事件通知
    final ConsumerMethodModel.AsyncMethodInfo asyncMethodInfo = methodModel.getAsyncInfo();
    if (asyncMethodInfo == null) {
        return null;
    }
    return asyncMethodInfo;
}
2.4.2 同步触发oninvoke事件

获取到调用方法对应的信息后,回到fireInvokeCallback()方法:

private void fireInvokeCallback(final Invoker<?> invoker, final Invocation invocation) {
    final ConsumerMethodModel.AsyncMethodInfo asyncMethodInfo = getAsyncMethodInfo(invoker, invocation);
    if (asyncMethodInfo == null) {
        return;
    }

    // 获取事件配置信息
    final Method onInvokeMethod = asyncMethodInfo.getOninvokeMethod();
    final Object onInvokeInst = asyncMethodInfo.getOninvokeInstance();

    if (onInvokeMethod == null && onInvokeInst == null) {
        return;
    }
    if (onInvokeMethod == null || onInvokeInst == null) {
        throw new IllegalStateException("service:" + invoker.getUrl().getServiceKey() + " has a oninvoke callback config , but no such " + (onInvokeMethod == null ? "method" : "instance") + " found. url:" + invoker.getUrl());
    }
    if (!onInvokeMethod.isAccessible()) {
        onInvokeMethod.setAccessible(true);
    }

    // 获取方法参数
    Object[] params = invocation.getArguments();
    try {
        // 触发oninvoke事件
        onInvokeMethod.invoke(onInvokeInst, params);
    } catch (InvocationTargetException e) {
        // 触发onthrow事件
        fireThrowCallback(invoker, invocation, e.getTargetException());
    } catch (Throwable e) {
        fireThrowCallback(invoker, invocation, e);
    }
}
2.4.3 调用结果处理

方法调用完成后,会回到postProcessResult()方法:

@Override
public Result postProcessResult(Result result, Invoker<?> invoker, Invocation invocation) {

    // 如果是异步调用,返回结果会被封装成AsyncRpcResult类型的对象,具体在哪里封装的,后面会讲到
    if (result instanceof AsyncRpcResult) {
        AsyncRpcResult asyncResult = (AsyncRpcResult) result;
        asyncResult.thenApplyWithContext(r -> {
            asyncCallback(invoker, invocation, r);
            return r;
        });
        return asyncResult;
    } else {
        syncCallback(invoker, invocation, result);
        return result;
    }
}

syncCallback和asyncCallback里面的逻辑比较简单,就是根据方法是正常返回还是抛异常,触发对应的事件。可以看到,如果被调用方法是同步的,则这两个事件也是同步的,反之亦然。

2.4.4 方法调用核心过程

在postProcessResult()方法中,第一个参数是invoker.invoke(invocation),这里就会走到下一个Filter链完成filter链的处理,最终调到原始服务,走到DubboInvoker#doInvoke方法:

protected Result doInvoke(final Invocation invocation) throws Throwable {
    ...
    try {
        // 读取async配置
        boolean isAsync = RpcUtils.isAsync(getUrl(), invocation);
        // 读取future_generated/future_returntype配置,还没搞明白是干啥的
        boolean isAsyncFuture = RpcUtils.isGeneratedFuture(inv) || RpcUtils.isFutureReturnType(inv);
        // 读取return配置
        boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
        int timeout = getUrl().getMethodParameter(methodName, Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT);
        if (isOneway) {
            // 如果配置return="true",future对象就直接设置为null
            boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);
            currentClient.send(inv, isSent);
            RpcContext.getContext().setFuture(null);
            return new RpcResult();
        } else if (isAsync) {
            // 如果配置async="true",构建future对象
            ResponseFuture future = currentClient.request(inv, timeout);
            // For compatibility
            FutureAdapter<Object> futureAdapter = new FutureAdapter<>(future);
            RpcContext.getContext().setFuture(futureAdapter);

            // 同时将返回结果包装为AsyncResult对象
            Result result;
            if (isAsyncFuture) {
                // register resultCallback, sometimes we need the asyn result being processed by the filter chain.
                result = new AsyncRpcResult(futureAdapter, futureAdapter.getResultFuture(), false);
            } else {
                result = new SimpleAsyncRpcResult(futureAdapter, futureAdapter.getResultFuture(), false);
            }
            return result;
        } else {
            // 否则就是同步调用,future当然也是null
            RpcContext.getContext().setFuture(null);
            return (Result) currentClient.request(inv, timeout).get();
        }
    }
    ...
}

通过这个过程不难发现,不管是同步调用还是异步调用,最终都会走到ExchangeClient#send方法,再往下会走到HeaderExchangeChannel#request方法,这个一个异步方法,返回ResponseFuture对象。

    @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.
        Request req = new Request();
        req.setVersion(Version.getProtocolVersion());
        req.setTwoWay(true);
        req.setData(request);
        DefaultFuture future = DefaultFuture.newFuture(channel, req, timeout);
        try {
            channel.send(req);
        } catch (RemotingException e) {
            future.cancel();
            throw e;
        }
        return future;
    }

看到这里我才恍然大悟,原来dubbo中同步调用也是通过异步调用来实现,只是同步调用发起后,直接调用future#get的方法来同步等待结果的返回,而异步调用只返回Future Response,在用户需要关心其结果时才调用get方法。

参考:
http://dubbo.apache.org/zh-cn/blog/dubbo-invoke.html
http://dubbo.apache.org/zh-cn/blog/dubbo-new-async.html
https://mp.weixin.qq.com/s?__biz=MzUzNTY4NTYxMA==&mid=2247484959&idx=5&sn=654b4ae76e76ac1a436f2ceb1774f4a6&chksm=fa80f69acdf77f8c5371d4a929557d6ec7fba3020c3bbb1490f0835218e8aef7af285e91d097&scene=21#wechat_redirect

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • 先看官网两张图【引用来自官网】:image.png 官网说明: 1.首先 ReferenceConfig 类的 i...
    致虑阅读 1,001评论 0 2
  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,022评论 8 265
  • 山外青天天愈蓝, 风追落花花渐远。 玉手轻裙翩翩舞, 疑是天降采花仙。
    远山一梦阅读 332评论 2 2
  • 今天呢我想跟大家说说我在工作中遇见的三个人。这三个人呢在我职业生涯的不同阶段,以不同的方式给予了极大的鼓励和启示。...
    天涯咫尺之间阅读 234评论 0 0