why哥这里有一道Dubbo高频面试题,请查收。

https://mp.weixin.qq.com/s/EG7pJH4vz-oVlRVHDOxoRQhtml



荒腔走板

你们好,我是 why,欢迎来到我连续周更优质原创文章的第 64 篇。老规矩,先荒腔走板聊聊其余的。
java

上面这图是我以前拼的一个拼图。面试

我常常玩拼图,我大概拼了 50 副左右的 1000 个小块的拼图,可是玩的都是背后有字母或者数字分区提醒的那种,最快纪录是一天拼完一副 1000 块的拼图。算法

可是上面这幅,只有 800 个小块,倒是我拼过的最难的一幅。由于这个背后没有任何提示,只能按照前面的色彩、花纹、边框进行一点点的拼凑。先后花了我两周多的时间。apache

这彻底是一种找虐的行为。编程

可是你知道这个拼图拼出来的图案叫什么吗?c#

坛城,传说中佛祖居住的地方。并发

第一次知道这个名词是 2015 年,窝在寝室看纪录片《第三极》。less

其中有一个片断讲的就是僧人为了某个节日用专门收集来的彩沙绘画坛城,他们的那种专一、虔诚、真挚深深的打动了我,当宏伟的坛城画完以后,它静静的等待节日的到来。异步

本觉得节日当天众人会对坛城顶礼膜拜,而实际状况是典礼开始的时候,你们手握一炷香,而后看着众僧人快速的用扫把摧毁坛城。

还没来得及仔细欣赏那复杂的美丽的图案,却又用扫把扫的干干净净。扫把扫下去的那一瞬间,个人心受到了一种强烈的撞击:能够辛苦地拿起,也能够轻松地放下。

那个画面对个人视觉冲击太大了,质本洁来还洁去。以致于我一下就紧紧的记住了这个词:坛城。

后来去了北京,在北京的出租屋里面,看着空荡荡的墙面,我想:要不拼个坛城吧,把北漂当作一场修行,应景。

拼的时候我又看了一遍《第三极》,看到摧毁坛城的片断的时候,有一个弹幕是这样说的:

一切有为法,如梦幻泡影,如露亦如电,应做如是观。

这句话出自《金刚般若波罗蜜经》第三十二品,应化非真分。以前翻阅过几回《金刚经》读到这里的时候我就以为这句话颇有哲理,可是也似懂非懂。因此印象比较深入。

当它再次以这样的形式展示在个人眼前的时候,我一下就懂了其中的哲理,不敢说大彻大悟,至少领悟一二。

观看摧毁坛城,这个色彩斑斓的世界变幻消失的过程,个人感觉是震撼,惋惜,放不下。

可是僧人却风轻云淡的说:一切有为法,如梦幻泡影,如露亦如电,应做如是观。

纪录片《第三极》,豆瓣评分 9.4 分,推荐给你。

好了,说回文章。

一道面试题

让咱们开门见山,直面主题:Dubbo 服务里面有个服务端,还有个消费端你知道吧?

服务端和消费端都各有一个线程池你知道吧?

那么面试题来了:通常状况下,服务提供者比服务消费者多吧。一个服务消费方可能会并发调用多个服务提供者,每一个用户线程发送请求后,会进行超时时间内的等待。多个服务提供者可能同时作完业务,而后返回,服务消费方的线程池会收到多个响应对象。这个时候要考虑一个问题,如何将线程池里面的每一个响应对象传递给相应等待的用户线程,且不出错呢?

先说答案。

这个题和答案其实就写在 Dubbo 的官网上:

http://dubbo.apache.org/zh-cn/docs/source_code_guide/service-invoking-process.html

如下回答来自官网:

答案是经过调用编号进行串联。

DefaultFuture 被建立时(下面咱们会讲这个 DefaultFuture 是个什么东西),会要求传入一个 Request 对象。

此时 DefaultFuture 可从 Request 对象中获取调用编号,并将 <调用编号, DefaultFuture 对象> 映射关系存入到静态 Map 中,即 FUTURES。

线程池中的线程在收到 Response 对象后,会根据 Response 对象中的调用编号到 FUTURES 集合中取出相应的 DefaultFuture 对象,而后再将 Response 对象设置到 DefaultFuture 对象中。

最后再唤醒用户线程,这样用户线程便可从 DefaultFuture 对象中获取调用结果了。整个过程大体以下图:

image.png

上面是官网上的答案,写的比较清楚了,可是官网上是在写服务调用过程的时候顺便讲解了这个考察点,源码散布在各处,看起来比较散乱,不太直观。有的读者反映看的不是特别的明白。

我知道你为何看的不是那么明白,我在以前的文章里面说过了,你根本就只是在官网白嫖,也不本身动手,像极了看我文章时候的样子:

image.png

好了,反正我也习惯被白嫖了,蹭我还写的动,大家就可劲嫖吧。

image.png

源码之中无秘密。带你从源码之中寻找答案,让你把官网上的回答和源码能对应起来,这样就更方便你本身动手了。

须要说明一下的是本文 Dubbo 源码版本为 2.7.5。而官网文档演示的源码版本是 2.6.4 。这两个版本上仍是有一点差别的,写到的地方我会进行强调。

Demo演示

Demo 你们能够直接参照官方的快速启动:

dubbo.apache.org/zh-cn/docs/user/quick-start.html

我这里就是一个很是简单的服务端:

image.png

客户端在单元测试里面进行消费:

image.png

是的,细心的老朋友确定看出来了,这个 Demo 我已经用过很是屡次了。基本上我每篇 Dubbo 相关的文章里面都会出现这个 Demo。

我建议你本身也花了 10 分钟时间搭一个吧。对你的学习有帮助。别懒,好吗?

我给你一个地址,而后你拉下来就能跑,这种也不是不行。这种我也考虑过。主要是治一治你不想本身动手的毛病,其次那不是我也懒得弄嘛。

image.png

好了,上面的 Demo 跑一下:

image.png

输出也是在咱们的意料之中。固然了,你们都知道这个输出也必须是这样的。

那么你再细细的品一品。

咱们扣一下题,把最开始的问题简化一下。

最开始的问题是一个服务消费端,多个服务提供者,而后服务提供者同时返回响应数据,消费端怎么处理。

其实核心问题就是服务消费端同时收到了多个响应数据,它应该怎么把响应数据对应的请求找到,只有正确找到了请求,才能正确返回数据。

因此咱们把重心放到客户端。

在上面的例子中:参数 why1 和 why2 几乎是同时发到服务端的请求 ,而后服务端对于这两个请求也几乎同时响应到了客户端。

在服务端没有返回的时候客户端的两个请求是在干什么?是否是在用户线程上里面等着的接收数据?

那么问题就来了:Dubbo 是怎么把这两个响应对象和两个等待接收数据的用户线程配对成功的?

接下来,咱们就带着这个问题,去源码里面寻找答案。

image.png

请求发起,等待响应

首先前面两节咱们都说到了客户端用户线程的等待,也就是一次请求在等待响应。

这个等待在代码里面是怎么体现的呢?

答案藏在这个方法里面:

org.apache.dubbo.rpc.protocol.AsyncToSyncInvoker#invoke

image.png

首先你看这个类名,AsyncToSyncInvoker,异步调用转同步调用,就感受不简单,里面确定搞事情了。

标号为 ① 的地方,是 invoker 调用,调用以后的返回是一个AsyncRpcResult。

在这个方法继续往下 Debug,没几步就能够走到这个地方:

org.apache.dubbo.remoting.exchange.support.header.HeaderExchangeChannel#request(java.lang.Object, int, java.util.concurrent.ExecutorService)

image.png

135 行就是 channel.send(req)。在往外发请求了,在发请求以前构建了一个 DefaultFuture。而后在请求发送出去以后,140 行返回了这个 future 。

最关键的秘密就藏在 133 行的这个 newFuture 里面。

看一看对应代码:

image.png

这个 newFuture 主要干了两件事:

  • 初始化 DefaultFuture 。

  • 检测是否超时。

咱们看看初始化 DefaultFuture 的时候干了啥事:

image.png

首先咱们在这里看到了 FUTURES 对象,这是一个 ConcurrentHashMap。这个 FUTURES 就是官网上说的静态 Map:

image.png

Map 里面的 key 是调用编号,也就是第 82 行代码中,从 request 里面得到的 id:

image.png

这个 id 是 AtomicLong 从 0 开始自增出来的。

代码里面还给了这样一行注释:

getAndIncrement() When it grows to MAX_VALUE, it will grow to MIN_VALUE, and the negative can be used as ID

说这个方法当增长到 MAX_VALUE 后再次调用会变成 MIN_VALUE。可是没有关系,负数也是能够当作 ID 来用的。

这个 DefaultFuture 对象构建完成后是返回回去了。

返回到哪里去呢?

就是 DubboInvoker 的 doInvoker 方法中下面框起来的代码:

image.png

在 103 行,包装以后的 DefaultFuture 会经过构造方法放到 AsyncRpcResult 对象中:

image.png

而 DubboInvoker 的 doInvoker 方法返回的这个 result,即 AsyncRpcResult 就是前面标号为 ① 这里的返回值:

image.png

接着说说标号为 ② 的地方。

首先是判断当前调用模式是不是同步调用。咱们这里就是同步调用,因而进入到 if 判断里面的逻辑。在这里面一看,调用的 get 方法,还带有超时时间。

看一下这个 get 方法是怎么样的:

image.png

能够看到这个 get 方法不是一个简单的异步编程的 CompletableFuture.get 。里面还包含了一个 ThreadlessExecutor 的 waitAndDrain 方法的逻辑。

这个方法一进来就是 queue.take 方法,阻塞等待。

这个队列里面装的是什么东西?

全局查找往这个队列里面放东西的逻辑,只有下面这一处:

image.png

说明这个队列里面扔的是一个 runable 的任务。

这个任务是什么呢?

咱们这里先买个关子,放到下一小节里面去讲。

你只要知道:若是队列里面没有任务,那么用户线程就会一直在 take 这里阻塞等待。

image.png

有的小伙伴就要问了:这里怎么能是阻塞式的无限等待呢?接口调用不是有超时时间吗?

注意了,这里并非无限等待。Dubbo 会保证当接口不论是否超时,都会有一个 Runable 的任务被扔到队列里面。因此 take 这里最多也就是等待超时时间这么长时间。

先记着这里,下面会给你们讲到超时检测的逻辑。

看到这里,咱们已经和官网上的回答产生一点联系了,我再给你们捋一捋咱们如今有的东西:

第一点:用户线程在 AsyncToSyncInvoker 类里面调用了下面这个方法,在等结果。代码和官网上的描述的对应关系以下:

image.png

官网上说:会调用不一样 DefaultFuture 对象的 get 方法进行等待,这应该是 2.6.x 版本的作法了。

在 2.7.5 版本中是在 AsyncRpcResult 对象的 get 方法中进行等待。而在该方法中,实际上是调用了队列的 take 方法,阻塞等待。

在这两个不一样对象上的等待是两种彻底不一样的实现方式。2.7.5 版本里面这样作也是为了作客户端的共享线程池。实现起来优雅了不少,你们能够拿着两个版本的代码自行比较一下,理解到他的设计思路以后以为真的是妙啊。

可是不论哪一个版本,万变不离其宗,请求发出去后,仍是须要在用户线程等待。

第二点:发送 request 对象以前构建了一个 DefaultFuture 对象。在这个对象里面维护了一个静态 MAP:

image.png

有了调用编号和 DefaultFuture 对象的映射关系。等收到 Response 响应以后,咱们从 Response 中取出这个调用编号,就知道这个调用编号对应的是哪一个 DefaultFuture 了,妙啊。

可是,等等。“从 Response 中取出这个调用编号”,那不是意外着咱们得把调用编号送到服务端去?在哪送的?

答案是在协议里面,还记得上一篇文章中讲协议的时候里面也有个调用编号吗?

image.png

呼应上了没有?

每一个请求和响应的 header 里面都有一个请求编号,这个编号是一一对应的,这是协议规定好的。

在发送 request 以前,对其进行 encode 的时候写进去的:

org.apache.dubbo.remoting.exchange.codec.ExchangeCodec#encodeRequest

image.png

而后 Dubbo 就拿着这个携带着 requestId 的请求这么轻轻的一发。

你猜怎么着?

image.png

就等着响应了。

接受响应,寻找请求

请求发出去是一件很简单的事情。

可是做为响应回来以后就懵逼。一个响应回来了,找不到是谁发起的它,你说它难受不难受?难受就算了,你就不怕它随便找一个请求就返回了,当场让你懵逼。

你说响应消息是在哪儿处理的?

上篇文章专门讲过哈,说不知道的都是假粉丝:

org.apache.dubbo.rpc.protocol.dubbo.DubboCodec#decodeBody

image.png

你看上门代码截图的第 66 行:get request id(获取请求编号)。

从哪里获取?

从 header 中获取。

header 中的请求编号是哪里来的?

发起 request 请求的时候,从 request 对象中取出来写到协议里面的。

request 对象中的请求编号是哪里来的?

经过 AtomicLong 从 0 开始自增来的。

好了,知道这个 id 是怎么来的了,也获取到了。它是在哪里用的呢?

org.apache.dubbo.remoting.exchange.support.DefaultFuture#received(org.apache.dubbo.remoting.Channel, org.apache.dubbo.remoting.exchange.Response, boolean)

image.png

标号为 ① 的地方就是根据 response 里面的 id,即调用编号从 FUTURES 这个 MAP 中移除并获取出对应的请求。

若是获取到的请求是 null,说明超时了。

若是获取到的请求不为 null,则判断是否超时了。超时逻辑咱们最后再讲。

标号为 ② 地方是要把响应返回给对应的用户线程了。

在 doReceived 里面使用了响应式编程:

image.png

这的 this 就是当前类,即 DefaultFuture。

那么这个 doReceived 方法是怎么调到这里的呢?

image.png

以前的文章说过 Dubbo 默认的派发策略是 ALL,因此全部的响应都会被派发到客户端线程池里面去,也就是这个地方:

当接收到服务端的响应后,响应事件也会被扔到线程池里面,从代码中能够看到,扔进去的就是一个 Runable 任务。

而后执行了 execute 方法,这个方法就和上一小节讲请求的地方呼应上了。

还记得咱们的请求是调用了 queue.take  方法,进入阻塞等待吗?

而这里就是在往 queue 里面添加任务。

image.png

队列里面有任务啦!在阻塞等待的用户线程就活过来了!

接下来用户线程怎么执行?

看代码:

image.png

取到任务后执行了任务的 run 方法。注意是 run 方法哦,并不会起新的线程。

而这个任务是什么任务?

image.png

是 ChannelEventRunnable。看一下这个任务重写的 run 方法:

image.png

这不是巧了吗,这不是?

image.png

上周的文章也说到了这个方法。

而 handler.received 方法最终就会调用到咱们前说的 doReceived 方法:

image.png

闭环完成。

因此当用户线程执行完这个 Runable 任务后,继续往下执行:

image.png

这里返回的 Result 就是最终的服务端返回的数据了,或者是返回的异常。

如今你再回过头去看官网这张图,应该就能看明白了:

image.png

超时检查

前面说 newFuture 的时候不是说它还干了一件事就是检测是否超时嘛。其实原理也是很简单:

image.png

首先有一个 TimeoutCheckTask 类,这是一个待执行的任务。

image.png

触发后会根据调用编号去 FUTURES 里面取 DefaultFuture。

前面我刚刚说了:若是一个 future 正常完成以后,会从 FUTURES 里面移除掉。

那么若是到点了,根据编号没有取到 Future 或者取到的这个 Future 的状态是 done 了,则说明这个请求没有超时。

若是这个 Future 还在 FUTURES 里面,含义就是到点了你咋还在里面呢?那确定是超时了,调用 notifyTimeout 方法,是否超时参数给 true:

image.png

这个 received 方法全局只有两个调用的地方,一个是前面讲的正常返回后的调用,一个就是这里超时以后的调用:

image.png

也就是不论怎样,最终都会调用这个 received 方法,最终都会经过这个方法把对应调用编号的 DefaultFuture 对象从 FUTURE 这个 MAP 中移除。

上面这个任务怎么触发呢?

Dubbo 本身搞了个 HashedWheelTimer ,这是什么东西?

时间轮调度算法呀:

image.png

你发起一个请求,指定时间内没有返回结果,因而就取消(future.cancel)这个请求。

这个需求不就相似于你下单买个东西,30 分钟尚未支付,因而平台自动给你取消了订单吗?

时间轮,能够解决你这个问题。以前的这篇文章中有介绍:《面试时遇到『看门狗』脖子上挂着『时间轮』,我就问你怕不怕?》

一个 2.7.5 版本关于检查 Dubbo 超时的小知识点,送给你们。

image.png

验证编号

前面一直在强调,这个调用编号很重要。

image.png

因此为了让你们有个更加直观的认识,我截个简单的图,给你们验证一下这个编号确实是贯穿请求和响应的。

首先,改造一下咱们的服务端:

image.png

当传进来的 name 是指定参数(why-debug)时,直接返回。不然都睡眠 10 秒,目的是让客户端用户线程一直等待响应。

客户端改造以下:

image.png

先连续发 40 个请求到服务端,对于这些请求服务端都须要 10 秒的时间才能处理完成。

而后再发生一个特定请求到服务端,能即便返回。并在 39 行打上断点。

首先,看一下 DefaultFuture 里面的调用编号。

没看以前,你先猜一下,当前 debug 的这个请求的调用编号是多少?

是否是 40 号(编号从 0 开始)?

来验证一下:

image.png

因此在发送请求的地方,在 header 里面设置调用编号为 40:

image.png

而后看一下响应回来以后,对应的调用编号是不是 40:

image.png

这样,一个调用编号,串联起了请求和响应。让请求必有回应,让响应一定能找到是哪一个请求发起的。

这就是:事事有回音。

image.png

最后说一句(求关注)

好了,看到了这里安排个一键三连(转发、在看、点赞)吧,周更很累的,不要白嫖我,须要一点正反馈

image.png

才疏学浅,不免会有纰漏,若是你发现了错误的地方,能够在留言区提出来,我对其加以修改。

感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。

image.png

我是 why,一个被代码耽误的文学创做者,不是大佬,可是喜欢分享,是一个又暖又有料的四川好男人。

还有,重要的事情说三遍:欢迎关注我呀。欢迎关注我呀。欢迎关注我呀。

image.png

相关文章
相关标签/搜索