在Dubbo中实现更简单易用的异步

Dubbo是Alibaba开源的一款很棒的SOA框架,被国内很多公司所采用。去哪儿网也早在2011年就开始采用Dubbo作为我们服务化拆分的基础,并且在Dubbo停止维护后自己fork了分支继续维护,也在Dubbo上投入了大量的精力。

作为服务拆分的基础设施,随着调用链路日趋复杂化,异步调用越来越重要了。

Dubbo默认配置下采用的是同步调用方式。也就是consumer调用一个服务的时候,当前线程会block住,block的线程将不能干其他事情。而线程是昂贵而有限的资源,比如在链路中consumer往往又是其他服务的provider: a -> b -> c。如果这个时候c因为某种原因响应时间恶化,使用同步调用的b将会受到严重的影响,最坏的情况下可能将b的服务线程耗光,最后b拒绝服务,进而对a造成影响。而如果c是一个非关键服务,而b提供的是关键服务,这个时候你肯定后悔莫及。而如果b是异步去调用c就不同了,b发起异步调用之后调用线程即可释放,可以继续处理别的事情了,这样如果是非关键服务可以干脆不用关心它的返回结果了。

上面描述的是consumer异步调用的情况,而provider也有异步处理的场景,比如上面的b如果采用异步调用c,那么如果c有返回结果了,什么时候将结果返回给a呢?Dubbo默认配置是同步等待provider的处理结果,然后将其写回。但是provider端如果是一个异步调用则无法等待结果了,因为发起异步调用后hold住连接的线程就离开了。所以provider端也需要一种机制,处理结果完成时将结果返回。

为了区分上面的两种情况我将其称为consumer端异步和provider端异步。

Dubbo默认也提供了『多种多样』的异步方式,但个人觉得这些异步方式略显混乱和难用。下面将简单的过一下Dubbo现有的一些异步方式。

consumer端异步

1. 使用Future的方式

a. 先要将该服务配置成异步调用

这也是我不喜欢的地方之一。是否异步调用应该是根据当前场景做出的选择,一个服务可能在有的地方使用同步调用,而在另外一些地方使用异步。而这种配置死的方式就很麻烦了,如果有两种用途还需要配置两个。640?wx_fmt=jpeg

b. 调用

640?wx_fmt=jpeg
一直觉得上面的API很反人类。service.longTask()是实际调用的地方,因为是异步调用你不能从这里拿返回结果,所以就孤孤单单的留在这里。还有上面的ResponseCallback,你千万别被那个caught所迷惑,这里只有超时以及框架异常才会调用,而业务异常是不会调用的,业务异常也是走done。而done里的response也不是你期望的返回结果,而是Dubbo里一个Result对象。

 

2. 配置回调的方式

640?wx_fmt=jpeg
第二种方式就不详细介绍了,就是异步调用结果返回的时候会调用callback对象的onResult方法。

上面就是Dubbo原有的consumer端异步的方式,然后我们再来看看provider端异步。

provider端异步

如果说consumer端异步的实现方式不友好,那么provider异步的方式就更加繁琐了,繁琐到你用一次就需要查一次文档。

首先如果要使用provider端异步,那么服务的API就需要做出改变。比如你原有的服务接口是:

String sayHello(String name);

那么你需要修改为:

void sayHello(String name, ResultListener listener);

这个ResultListener是你自定义的一个接口,而不是真正的参数。然后对服务的配置要做进一步修改:

640?wx_fmt=jpeg
这上面的参数还有一些坑,懒得介绍了。

然后consumer调用的时候:

640?wx_fmt=jpeg
provider端的实现逻辑:

640?wx_fmt=jpeg
这种方式,每次使用都很不舒服。首先我觉得provider是否异步并不关consumer的事情,不应该对consumer产生任何影响。比如我有个抓取服务,原来使用的是同步的httpclient去访问别的系统,然后有一天我修改为异步httpclient,那我是否要consumer协助我一起修改呢?

以上就是Dubbo的provider异步了。

有了这么多痛点,我们决定重构一下Dubbo的异步实现方式。

 

新的异步

consumer端异步

实现一套新的API,我还是喜欢从上而下,也就是先写出我喜欢的调用方式,然后再去想想如何去实现:

640?wx_fmt=jpeg
我希望consumer端异步最好就这样了,而且不用任何预先配置,我想异步就异步,想同步就同步。这样一个API还有一个好处是可以和现有的工具很好的结合,比如google Guava里一整套针对ListenableFuture的处理类。

但是,sayHello的原始签名是这样的: String sayHello(String name)。怎么让它返回一个Future呢?难道每个API都要写两个版本么?写两个版本那provider如何去实现呢?

我们在前文已经说过了,consumer端的异步和provider端并无关系,只是consumer在调用的时候决定是否等待。那么我们现在我们要做的就是怎么弄出这样一个API出来,然后在调用的时候里面偷偷的异步去调用provider。

我们的实现方式是使用java的annotation processor,你只需要在你原有的API上添加这么一个注解:

640?wx_fmt=jpeg
然后我们实现一个annotation processor自动给你生成一个这个接口的实现接口:


这个接口是自动生成的,provider提供这个接口的实现,甚至不用关心这个接口的存在。然后consumer端使用的时候就可以直接用这个接口了:

640?wx_fmt=jpeg
 

640?wx_fmt=jpeg
因为这个接口实现了同步版本的接口,所以即可以调用异步,也可以调用同步。这样一来,API的变通就搞定了,实现起来也很简单。Dubbo是通过字节码生成的方式来生成网络代理,进行rpc调用的。所以给异步的方法生成的代理只要是异步调用就行了,然后其实里面实际调用的还是同步版本的接口,只不过调用方式变了,就是调用的时候不去wait,而是返回一个Future。而Dubbo里只是用API去做服务发现,去找到provider的地址列表,所以在服务发现的时候我们从AsyncImpl里取出对应的同步接口去找provider列表。

这个的方式是不是比Dubbo原生的方式漂亮多了呢?遵循习惯的模式,API所见即所得。解决了consumer端异步,我们再来看看provider端异步吧。

 

provider端异步

其实provider端异步也已经有可参照的API了: servlet 3.0里的async servlet。类似下面的方式(伪代码):

640?wx_fmt=jpeg
那么我们就可以模仿这个对Dubbo的API做一下改进:

640?wx_fmt=jpeg
这样的API对consumer完全是透明的,consumer可以同步调用这个方法,也可以异步调用这个方法。而Dubbo内部的改造也很简单。其实AsyncContext就像是一个ListenableFuture,当执行完provider的逻辑后,给这个Future注册一个回调,在回调里将结果写回consumer即可(伪代码):

640?wx_fmt=jpeg
OK,这就是我们改造后的Dubbo异步版本API了,其实对Dubbo本身的改造才几行代码,很容易就实现了。

实际上,在设计API的时候,我们应该更多的从使用方考虑,先将使用API的demo写出来,然后再思考实现。另外,实现API的时候尽量遵循惯用法,可以看看有没有其他的项目里已有这种做法,特别是那些广为人知的项目,比如这里的guava和servlet 3.0之类的。

转载于:https://my.oschina.net/xiaominmin/blog/1594898

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值