一道面試題
讓咱們開門見山,直面主題:Dubbo 服務裏面有個服務端,還有個消費端你知道吧?
html
服務端和消費端都各有一個線程池你知道吧?java
那麼面試題來了:通常狀況下,服務提供者比服務消費者多吧。一個服務消費方可能會併發調用多個服務提供者,每一個用戶線程發送請求後,會進行超時時間內的等待。多個服務提供者可能同時作完業務,而後返回,服務消費方的線程池會收到多個響應對象。這個時候要考慮一個問題,如何將線程池裏面的每一個響應對象傳遞給相應等待的用戶線程,且不出錯呢?web
先說答案。面試
這個題和答案其實就寫在 Dubbo 的官網上:算法
http://dubbo.apache.org/zh-cn/docs/source_code_guide/service-invoking-process.html
apache
如下回答來自官網:編程
答案是經過調用編號進行串聯。c#
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:
這樣,一個調用編號,串聯起了請求和響應。讓請求必有迴應,讓響應一定能找到是哪一個請求發起的。
這就是:事事有迴音。
最後說一句(求關注)
好了,看到了這裏安排個「一鍵三連」(轉發、在看、點贊)吧
全乾貨技術公衆號Java學習指南
👇👇
長按上方二維碼關注公衆號
本文分享自微信公衆號 - Java學習指南(gh_85b94beaede2)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。