why哥這裏有一道Dubbo高頻面試題,請查收。

[
文件](https://www.mdnice.com/#)格式功能查看主題代碼主題設置幫助html

這是why的第 64 篇原創文章java

荒腔走板

你們好,我是 why,歡迎來到我連續周更優質原創文章的第 64 篇。老規矩,先荒腔走板聊聊其餘的。面試

上面這圖是我以前拼的一個拼圖。算法

我常常玩拼圖,我大概拼了 50 副左右的 1000 個小塊的拼圖,可是玩的都是背後有字母或者數字分區提醒的那種,最快紀錄是一天拼完一副 1000 塊的拼圖。apache

可是上面這幅,只有 800 個小塊,倒是我拼過的最難的一幅。由於這個背後沒有任何提示,只能按照前面的色彩、花紋、邊框進行一點點的拼湊。先後花了我兩週多的時間。編程

這徹底是一種找虐的行爲。c#

可是你知道這個拼圖拼出來的圖案叫什麼嗎?segmentfault

壇城,傳說中佛祖居住的地方。併發

第一次知道這個名詞是 2015 年,窩在寢室看紀錄片《第三極》。less

其中有一個片斷講的就是僧人爲了某個節日用專門收集來的彩沙繪畫壇城,他們的那種專一、虔誠、真摯深深的打動了我,當宏偉的壇城畫完以後,它靜靜的等待節日的到來。

本覺得節日當天衆人會對壇城頂禮膜拜,而實際狀況是典禮開始的時候,你們手握一炷香,而後看着衆僧人快速的用掃把摧毀壇城。

還沒來得及仔細欣賞那複雜的美麗的圖案,卻又用掃把掃的乾乾淨淨。掃把掃下去的那一瞬間,個人心受到了一種強烈的撞擊:能夠辛苦地拿起,也能夠輕鬆地放下。

那個畫面對個人視覺衝擊太大了,質本潔來還潔去。以致於我一下就緊緊的記住了這個詞:壇城。

這是why的第 64 篇原創文章

荒腔走板

你們好,我是 why,歡迎來到我連續周更優質原創文章的第 64 篇。老規矩,先荒腔走板聊聊其餘的。

上面這圖是我以前拼的一個拼圖。

我常常玩拼圖,我大概拼了 50 副左右的 1000 個小塊的拼圖,可是玩的都是背後有字母或者數字分區提醒的那種,最快紀錄是一天拼完一副 1000 塊的拼圖。

可是上面這幅,只有 800 個小塊,倒是我拼過的最難的一幅。由於這個背後沒有任何提示,只能按照前面的色彩、花紋、邊框進行一點點的拼湊。先後花了我兩週多的時間。

這徹底是一種找虐的行爲。

可是你知道這個拼圖拼出來的圖案叫什麼嗎?

壇城,傳說中佛祖居住的地方。

第一次知道這個名詞是 2015 年,窩在寢室看紀錄片《第三極》。

其中有一個片斷講的就是僧人爲了某個節日用專門收集來的彩沙繪畫壇城,他們的那種專一、虔誠、真摯深深的打動了我,當宏偉的壇城畫完以後,它靜靜的等待節日的到來。

本覺得節日當天衆人會對壇城頂禮膜拜,而實際狀況是典禮開始的時候,你們手握一炷香,而後看着衆僧人快速的用掃把摧毀壇城。

還沒來得及仔細欣賞那複雜的美麗的圖案,卻又用掃把掃的乾乾淨淨。掃把掃下去的那一瞬間,個人心受到了一種強烈的撞擊:能夠辛苦地拿起,也能夠輕鬆地放下。

那個畫面對個人視覺衝擊太大了,質本潔來還潔去。以致於我一下就緊緊的記住了這個詞:壇城。

後來去了北京,在北京的出租屋裏面,看着空蕩蕩的牆面,我想:要不拼個壇城吧,把北漂當作一場修行,應景。

拼的時候我又看了一遍《第三極》,看到摧毀壇城的片斷的時候,有一個彈幕是這樣說的:

一切有爲法,如夢幻泡影,如露亦如電,應做如是觀。

這句話出自《金剛般若波羅蜜經》第三十二品,應化非真分。以前翻閱過幾回《金剛經》讀到這裏的時候我就以爲這句話頗有哲理,可是也似懂非懂。因此印象比較深入。

當它再次以這樣的形式展示在個人眼前的時候,我一下就懂了其中的哲理,不敢說大徹大悟,至少領悟一二。

觀看摧毀壇城,這個色彩斑斕的世界變幻消失的過程,個人感覺是震撼,惋惜,放不下。

可是僧人卻風輕雲淡的說:一切有爲法,如夢幻泡影,如露亦如電,應做如是觀。

紀錄片《第三極》,豆瓣評分 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 對象中獲取調用結果了。整個過程大體以下圖:

上面是官網上的答案,寫的比較清楚了,可是官網上是在寫服務調用過程的時候順便講解了這個考察點,源碼散佈在各處,看起來比較散亂,不太直觀。有的讀者反映看的不是特別的明白。

我知道你爲何看的不是那麼明白,我在以前的文章裏面說過了,你根本就只是在官網白嫖,也不本身動手,像極了看我文章時候的樣子:

好了,反正我也習慣被白嫖了,蹭我還寫的動,大家就可勁嫖吧。

源碼之中無祕密。帶你從源碼之中尋找答案,讓你把官網上的回答和源碼能對應起來,這樣就更方便你本身動手了。

須要說明一下的是本文 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 的時候寫進去的:

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

而後 Dubbo 就拿着這個攜帶着 requestId 的請求這麼輕輕的一發。

你猜怎麼着?

就等着響應了。

接收響應,尋找請求

請求發出去是一件很簡單的事情。

可是做爲響應回來以後就懵逼。一個響應回來了,找不到是誰發起的它,你說它難受不難受?難受就算了,你就不怕它隨便找一個請求就返回了,當場讓你懵逼。

你說響應消息是在哪兒處理的?

上篇文章專門講過哈,說不知道的都是假粉絲:

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

你看上門代碼截圖的第 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)

標號爲 ① 的地方就是根據 response 裏面的 id,即調用編號從 FUTURES 這個 MAP 中移除並獲取出對應的請求。

若是獲取到的請求是 null,說明超時了。

若是獲取到的請求不爲 null,則判斷是否超時了。超時邏輯咱們最後再講。

標號爲 ② 地方是要把響應返回給對應的用戶線程了。

在 doReceived 裏面使用了響應式編程:

這的 this 就是當前類,即 DefaultFuture。

那麼這個 doReceived 方法是怎麼調到這裏的呢?

以前的文章說過 Dubbo 默認的派發策略是 ALL,因此全部的響應都會被派發到客戶端線程池裏面去,也就是這個地方:

當接收到服務端的響應後,響應事件也會被扔到線程池裏面,從代碼中能夠看到,扔進去的就是一個 Runable 任務。

而後執行了 execute 方法,這個方法就和上一小節講請求的地方呼應上了。

還記得咱們的請求是調用了 queue.take 方法,進入阻塞等待嗎?

而這裏就是在往 queue 裏面添加任務。

隊列裏面有任務啦!在阻塞等待的用戶線程就活過來了!

接下來用戶線程怎麼執行?

看代碼:

取到任務後執行了任務的 run 方法。注意是 run 方法哦,並不會起新的線程。

而這個任務是什麼任務?

是 ChannelEventRunnable。看一下這個任務重寫的 run 方法:

這不是巧了嗎,這不是?

上週的文章也說到了這個方法。

而 handler.received 方法最終就會調用到咱們前說的 doReceived 方法:

閉環完成。

因此當用戶線程執行完這個 Runable 任務後,繼續往下執行:

這裏返回的 Result 就是最終的服務端返回的數據了,或者是返回的異常。

如今你再回過頭去看官網這張圖,應該就能看明白了:

超時檢查

前面說 newFuture 的時候不是說它還幹了一件事就是檢測是否超時嘛。其實原理也是很簡單:

首先有一個 TimeoutCheckTask 類,這是一個待執行的任務。

觸發後會根據調用編號去 FUTURES 裏面取 DefaultFuture。

前面我剛剛說了:若是一個 future 正常完成以後,會從 FUTURES 裏面移除掉。

那麼若是到點了,根據編號沒有取到 Future 或者取到的這個 Future 的狀態是 done 了,則說明這個請求沒有超時。

若是這個 Future 還在 FUTURES 裏面,含義就是到點了你咋還在裏面呢?那確定是超時了,調用 notifyTimeout 方法,是否超時參數給 true:

這個 received 方法全局只有兩個調用的地方,一個是前面講的正常返回後的調用,一個就是這裏超時以後的調用:

也就是不論怎樣,最終都會調用這個 received 方法,最終都會經過這個方法把對應調用編號的 DefaultFuture 對象從 FUTURE 這個 MAP 中移除。

上面這個任務怎麼觸發呢?

Dubbo 本身搞了個 HashedWheelTimer ,這是什麼東西?

時間輪調度算法呀:

你發起一個請求,指定時間內沒有返回結果,因而就取消(future.cancel)這個請求。

這個需求不就相似於你下單買個東西,30 分鐘尚未支付,因而平臺自動給你取消了訂單嗎?

時間輪,能夠解決你這個問題。以前的這篇文章中有介紹:《面試時遇到『看門狗』脖子上掛着『時間輪』,我就問你怕不怕?》

一個 2.7.5 版本關於檢查 Dubbo 超時的小知識點,送給你們。

驗證編號

前面一直在強調,這個調用編號很重要。

因此爲了讓你們有個更加直觀的認識,我截個簡單的圖,給你們驗證一下這個編號確實是貫穿請求和響應的。

首先,改造一下咱們的服務端:

當傳進來的 name 是指定參數(why-debug)時,直接返回。不然都睡眠 10 秒,目的是讓客戶端用戶線程一直等待響應。

客戶端改造以下:

先連續發 40 個請求到服務端,對於這些請求服務端都須要 10 秒的時間才能處理完成。

而後再發生一個特定請求到服務端,能即便返回。並在 39 行打上斷點。

首先,看一下 DefaultFuture 裏面的調用編號。

沒看以前,你先猜一下,當前 debug 的這個請求的調用編號是多少?

是否是 40 號(編號從 0 開始)?

來驗證一下:

因此在發送請求的地方,在 header 裏面設置調用編號爲 40:

而後看一下響應回來以後,對應的調用編號是不是 40:

這樣,一個調用編號,串聯起了請求和響應。讓請求必有迴應,讓響應一定能找到是哪一個請求發起的。

才疏學淺,不免會有紕漏,若是你發現了錯誤的地方,能夠在留言區提出來,我對其加以修改。

感謝您的閱讀,我堅持原創,十分歡迎並感謝您的關注。

我是 why,一個被代碼耽誤的文學創做者,不是大佬,可是喜歡分享,是一個又暖又有料的四川好男人。

相關文章
相關標籤/搜索