先说答案。

这个题和答案其实就写在 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 对象中获取调用结果了。整个过程大致如下图:

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

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

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

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

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

Demo演示

Demo 大家可以直接参照官方的快速启动:

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

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

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

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

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

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

好了,上面的 Demo 跑一下:

输出也是在我们的意料之中。当然了,大家都知道这个输出也必须是这样的。

那么你再细细的品一品。

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

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

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

所以我们把重心放到客户端。

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

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

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

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

请求发起,等待响应

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

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

答案藏在这个方法里面:

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

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

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

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

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

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

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

看一看对应代码:

这个 newFuture 主要干了两件事:

初始化 DefaultFuture 。

检测是否超时。

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

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

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

这个 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 方法中下面框起来的代码:

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

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

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

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

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

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

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

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

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

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

这个任务是什么呢?

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

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

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

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

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

看到这里,我们已经和官网上的回答产生一点联系了,我再给大家捋一捋我们现在有的东西:

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

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

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

在这两个不同对象上的等待是两种完全不同的实现方式。2.7.5 版本里面这样做也是为了做客户端的共享线程池。实现起来优雅了很多,大家可以拿着两个版本的代码自行比较一下,理解到他的设计思路之后觉得真的是妙啊。

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

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

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

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

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

呼应上了没有?

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

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

小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Java工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)

总结

一般像这样的大企业都有好几轮面试,所以自己一定要花点时间去收集整理一下公司的背景,公司的企业文化,俗话说「知己知彼百战不殆」,不要盲目的去面试,还有很多人关心怎么去跟HR谈薪资。

这边给大家一个建议,如果你的理想薪资是30K,你完全可以跟HR谈33~35K,而不是一下子就把自己的底牌暴露了出来,不过肯定不能说的这么直接,比如原来你的公司是25K,你可以跟HR讲原来的薪资是多少,你们这边能给到我的是多少?你说我这边希望可以有一个20%涨薪。

最后再说几句关于招聘平台的,总之,简历投递给公司之前,请确认下这家公司到底咋样,先去百度了解下,别被坑了,每个平台都有一些居心不良的广告党等着你上钩,千万别上当!!!

Java架构学习资料,学习技术内容包含有:Spring,Dubbo,MyBatis, RPC, 源码分析,高并发、高性能、分布式,性能优化,微服务 高级架构开发等等。

还有Java核心知识点+全套架构师学习资料和视频+一线大厂面试宝典+面试简历模板可以领取+阿里美团网易腾讯小米爱奇艺快手哔哩哔哩面试题+Spring源码合集+Java架构实战电子书。

些居心不良的广告党等着你上钩,千万别上当!!!

Java架构学习资料,学习技术内容包含有:Spring,Dubbo,MyBatis, RPC, 源码分析,高并发、高性能、分布式,性能优化,微服务 高级架构开发等等。

还有Java核心知识点+全套架构师学习资料和视频+一线大厂面试宝典+面试简历模板可以领取+阿里美团网易腾讯小米爱奇艺快手哔哩哔哩面试题+Spring源码合集+Java架构实战电子书。 [外链图片转存中…(img-V0aN6Oyp-1710434979575)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

推荐文章

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: