RPC 框架 Dubbo 從理解到使用(一)
技術架構演變
單一應用架構
通俗地講,“單體應用(monolith application)”就是將應用程式的所有功能都打包成一個獨立的單元。當網站流量很小時,只需一個應用,將所有功能都部署在一起,以減少部署節點和成本。
特點
所有的功能整合在一個專案工程中; 所有的功能打一個 war 包部署到伺服器; 應用與資料庫分開部署; 通過部署應用叢集和資料庫叢集來提高系統的效能。
優點:
「開發簡單」:一個 IDE 就可以快速構建單體應用;
「便於共享」:單個歸檔檔案包含所有功能,便於在團隊之間以及不同的部署階段之間共享;
「易於測試」:單體應用一旦部署,所有的服務或特性就都可以使用了,這簡化了測試過程,因為沒有額外的依賴,每項測試都可以在部署完成後立刻開始;
「容易部署」:整個專案就一個 war 包,Tomcat 安裝好之後,應用扔上去就行了。群化部署也很容易,多個 Tomcat + 一個 Nginx 分分鐘搞定。
缺點:
「妨礙持續交付」:隨著時間的推移,單體應用可能會變得比較大,構建和部署時間也相應地延長,不利於頻繁部署,阻礙持續交付。在移動應用開發中,這個問題會顯得尤為嚴重; 「不夠靈活」:隨著專案的逐漸變大,整個開發流程的時間也會變得很長,即使在僅僅更改了一行程式碼的情況下,軟體開發人員需要花費幾十分鐘甚至超過一個小時的時間對所有程式碼進行編譯,並接下來花費大量的時間重新部署剛剛生成的產品,以驗證自己的更改是否正確。如果多個開發人員共同開發一個應用程式,那麼還要等待其他開發人員完成了各自的開發。這降低了團隊的靈活性和功能交付頻率; 「受技術棧限制」:專案變得越來越大的同時,我們的應用所使用的技術也會變得越來越多。這些技術有些是不相容的,就比如在一個專案中大範圍地混合使用 C++ 和 Java 幾乎是不可能的事情。在這種情況下,我們就需要拋棄對某些不相容技術的使用,而選擇一種不是那麼適合的技術來實現特定的功能。 「可靠性差」:某個環節出現了死迴圈,導致記憶體溢位,會影響整個專案掛掉。 「伸縮性差」:系統的擴容只能針對應用進行擴容,不能做到對某個功能進行擴容,擴容後必然帶來資源浪費的問題。 「技術債務」:假設我的程式碼庫中有一個混亂的模組結構。此時,我需要新增一個新功能。如果這個模組結構清晰,可能我只需要2天時間就可以新增好這個功能,但是如今這個模組的結構很混亂,所以我需要4天時間。多出來的這兩天就是債務利息。隨著時間推移、人員變動,技術債務必然也會隨之增多。
垂直應用架構
當訪問量逐漸增大,單一應用增加機器帶來的加速度越來越小,將應用拆成互不相干的幾個應用,以提升效率。
特點
以單體結構規模的專案為單位進行垂直劃分,就是將一個大專案拆分成一個一個單體結構專案。 專案與專案之間存在資料冗餘,耦合性較大,比如上圖中三個專案都存在使用者資訊。 專案之間的介面多為資料同步功能,如:資料庫之間的資料庫,通過網路介面進行資料庫同步。
優點
開發成本低,架構簡單;
避免單體應用的無限擴大;
系統拆分實現了流量分擔,解決了併發問題;
可以針對不同系統進行擴容、優化;
方便水平擴充套件,負載均衡,容錯率提高;
不同的專案可採用不同的技術;
系統間相互獨立。
缺點
系統之間相互呼叫,如果某個系統的埠或者 IP 地址發生改變,呼叫系統需要手動變更; 垂直架構中相同邏輯程式碼需要不斷的複製,不能複用。 系統性能擴充套件只能通過擴充套件叢集結點,成本高、有瓶頸。
SOA 面向服務架構
當垂直應用越來越多,應用之間互動不可避免,將核心業務抽取出來,作為獨立的服務,逐漸形成穩定的服務中心。當服務越來越多,容量的評估,小服務資源的浪費等問題逐漸顯現,此時需增加一個排程中心基於訪問壓力實時管理叢集容量,提高叢集利用率。
❝P.S. 從軟體設計的角度上來說,ESB 是一個抽象的間接層,提取了服務呼叫過程中呼叫與被呼叫動態互動中的一些共同的東西,減輕了服務呼叫者的負擔。Java 程式設計思想裡提到:“所有的軟體設計的問題都可以通過增加一個抽象的間接層而得到解決或者得到簡化!”簡單來說 ESB 就是一根管道,用來連線各個服務節點。為了整合不同系統,不同協議的服務,ESB 做了訊息的轉化解釋和路由工作,讓不同的服務互聯互通。
❞
特點
基於 SOA 的架構思想將重複公用的功能抽取為元件,以服務的形式給各系統提供服務。 各專案(系統)與服務之間採用 WebService、RPC 等方式進行通訊。 使用 ESB 企業服務匯流排作為專案與服務之間通訊的橋樑。
優點
將重複的功能抽取為服務,提高開發效率,提高系統的可重用性、可維護性。 可以針對不同服務的特點制定叢集及優化方案; 採用 ESB 減少系統中的介面耦合。
缺點
系統與服務的界限模糊,不利於開發及維護。 雖然使用了 ESB,但是服務的介面協議不固定,種類繁多,不利於系統維護。 抽取的服務的粒度過大,系統與服務之間耦合性高。 涉及多種中介軟體,對開發人員技術棧要求高。 服務關係複雜,運維、測試部署困難
微服務架構
特點
將系統服務層完全獨立出來,並將服務層抽取為一個一個的微服務。 微服務中每一個服務都對應唯一的業務能力,遵循單一原則。 微服務之間採用 RESTful 等輕量協議傳輸。
優點
團隊獨立:每個服務都是一個獨立的開發團隊,這個小團隊可以是 2 到 5 人的開發人員組成; 技術獨立:採用去中心化思想,服務之間採用 RESTful 等輕量協議通訊,使用什麼技術什麼語言開發,別人無需干涉; 前後端分離:採用前後端分離開發,提供統一 Rest 介面,後端不用再為 PC、移動端開發不同介面; 資料庫分離:每個微服務都有自己的儲存能力,可以有自己的資料庫。也可以有統一資料庫; 服務拆分粒度更細,有利於資源重複利用,提高開發效率; 一個團隊的新成員能夠更快投入生產; 微服務易於被一個開發人員理解,修改和維護,這樣小團隊能夠更關注自己的工作成果。無需通過合作才能體現價值; 可以更加精準的制定每個服務的優化方案(比如擴充套件),提高系統可維護性; 適用於網際網路時代,產品迭代週期更短。
缺點
微服務過多,服務治理成本高,不利於系統維護; 分散式系統開發的技術成本高(網路問題、容錯問題、呼叫關係、分散式事務等),對團隊挑戰大; 微服務將原來的函式式呼叫改為服務呼叫,不管是用 rpc,還是 http rest 方式,都會增大系統整體延遲。這個是再所難免的,這個就需要我們將原來的序列程式設計改為併發程式設計甚至非同步程式設計,增加了技術門檻; 多服務運維難度,隨著服務的增加,運維的壓力也在增大; 測試的難度提升。服務和服務之間通過介面來互動,當介面有改變的時候,對所有的呼叫方都是有影響的,這時自動化測試就顯得非常重要了,如果要靠人工一個個介面去測試,那工作量就太大了,所以 API 文件的管理尤為重要。
總結
分享兩個小故事,幫助大家更好的理解 SOA 與微服務的區別。
故事一:
❝很久以前的一天,Martin 在跟好友的交流中悟到了一種很棒的架構設計。他總結了一下,然後告訴了好友,好友說,這不是新鮮東西,早有人總結了,叫做 SOA。
Martin 很高興,開始在公司內外推廣 SOA。結果,不停有人質疑和挑戰他。
你這不是 SOA 吧?SOA 這裡應該是如此這般的。對,這裡我對 SOA 的理解是這樣的。你看,這本 SOA 的書說的和你說的有出入。粒度?SOA 沒有談到這個呀,你這不是 SOA。分層跟 SOA 沒有關係,你為什麼要說這個呢?…
Martin 沒辦法,心一橫,老子就叫它 Martin's SOA。老子發明的詞,老子就是最高權威,有最終解釋權。還有誰不服?
同事們一看,這思想本身很不錯,值得推廣,但叫 Martin's SOA 太沒品了吧?還是要取個好一點的名字,最好還能跟 SOA 有某種暗示傳承。乾脆就叫 Microservices 好了,又新,又有服務含在其中。
Martin 忿忿地接受了這個建議,心裡想著:媽的,明明就是 SOA,一群傻逼非要逼我取個新名字。
後來 Martin 發現每次提一個東西就會收到舊惡傻勢力對解釋權的挑戰,所以每次要提一個什麼概念,都去發明一個新詞,免得一群人在那一邊質疑挑戰,一邊大談“我的理解”。
這就是微服務、敏捷、精益企業、持續整合、持續交付背後的故事。
一個名詞產生之後,命名者的解釋權就會隨著時間而弱化(比如 Cooper 發明的 Persona 就被無數設計師亂用)。敏捷已經有點爛了,等微服務也爛了,我們還會發明新詞。
實在沒轍,都是被逼的啊。
❞
故事二:
❝話說1979年,又是一個春天,莆田鄉下的赤腳醫生吳大牛被改革的春風吹的心潮澎湃,說幹就幹,吳大牛趁著夜色朦朧找大隊支書彙報了彙報思想,第二天就承包了村衛生室,開啟了自己的在醫療圈的傳奇歷程。
鄉村診所大家都知道,沒什麼複雜的東東,房子只有一間,一個大櫃檯中間隔開,一半是診療兼候診區,一半是藥房,看病就直接找醫生,如果前面有人就自己找個位子坐下,排隊等一會,秩序倒也井然,看完病了醫生直接給抓藥,然後下一個繼續,也不需要護士和藥劑師,吳大牛一個人全部包辦。
辛辛苦苦忙碌了十年,時間來到了八九年,又是一個春天,昔日的單身漢吳大牛已成為十里八鄉的知名人物,媳婦娶上了不說,家裡還增加了一對雙胞胎兒子,二層的小洋房也甚是氣派。可是也有煩心事,儘管鄉村診所擴大到了兩間,媳婦還偶爾能去幫幫忙,但是醫生還是隻有自己一個,天天從早忙到晚掙的都是一份錢,想多掙點怎麼辦?吳大牛日思夜想,還真給他想出來一招,怎麼辦,擴大規模,多招幾個醫生一起幹。原來吳大牛隻能治頭疼腦熱和跌打損傷,現在新招了一個醫科大學的畢業生劉小明專治感冒發燒,又從鄰村請來了老大夫李阿花專治婦科病,現在一個普通的小診所就變成了有三個獨立科室加一個公共藥房(吳大牛媳婦負責)的小醫院了,吳大牛是外科主任兼院長,收入那可比之前翻了三番。人逢喜事精神爽,大牛院長請縣裡的書法名家為新醫院書寫了牌匾--“博愛醫院”,挑了一個黃道吉日正式掛了上去。
一晃十年過去了,又是一個春天,吳大牛的博愛醫院已經發展到了內科外科婦科五官科骨科生殖科六個科室,每個科室3到5名醫生不等,也耗費巨資購進了血液化驗B超等先進儀器,大牛院長也早已脫離了醫療一線,成為了專職的管理者,但是醫院的大事小事大家都找他,就這三十多號員工搞的他每天是焦頭爛額,想再擴大規模實在是有心無力了。要說還是大學生有水平,老部下劉小明給大牛院長獻了一計,把各個科室獨立出去,讓各個科室主任自己管理,大牛院長只管科室之間的協調和醫院發展的大事,這樣既能調動基層的積極性,又能把大牛院長解放出來擴大生產抓大事謀大事,豈不妙哉?就這樣,博愛醫院的新一輪改革轟轟烈烈的展開了。
又是一個十年,又是一個春天,大牛院長已成為本地知名的企業家,博愛醫院也發展到了二十三個科室數百名員工,發展中也出現了新問題,由於各個科室獨立掛號、收費、化驗,有的科室整天忙忙碌碌效益好,有的科室就相對平庸些,連分到的各種檢查儀器都不能滿負荷執行,整個醫院養了不少閒人。這時候大牛院長視野也開闊了,請來了管理專家進行了頂層設計,把原來分散到各個科室的非核心服務全部收歸集中管理,把原來二十三個掛號視窗整合為十個,二十三個收費視窗整合為八個,集中佈設在一樓大廳為全院服務,還把分散在各個科室的檢查儀器集中起來成立獨立的檢驗科,也為全院服務,這樣人人有活幹,整個醫院的服務能力又上了一個新臺階,這輪改革後博愛醫院通過了各級部門的鑑定成為了遠近馳名的三甲醫院,吳大牛也換身一變成為了博愛集團的CEO兼董事長,下一步就準備IPO上市了。
說到這裡大家可能有點糊塗,這個跟微服務有嘛關係?在孫老師看來,大牛診所的1.0階段就相當於軟體開發的單體結構,一個程式設計師打天下,從頭編到尾,很難做大做強。大牛診所的2.0階段就相當於軟體開發的垂直結構,各科室按照業務劃分,很容易橫向擴充套件。博愛醫院的1.0階段就相當於軟體開發的SOA結構,除了藥房(資料庫)外各個服務獨立提供(科室主任負責),但需要大牛院長(ESB匯流排)來協調。博愛醫院的2.0階段就相當於軟體開發的微服務結構,公共服務院內共享,科室主任管理功能弱化(只管醫生業務),優點是擴容方便,哪個部門缺人直接加,不用看上下游,資源利用率高,人員和裝置效率高。
❞
微服務就是將一個單體架構的應用按業務劃分為一個個的獨立執行的程式即服務,它們之間通過 HTTP 協議進行通訊(也可以採用訊息佇列來通訊,如 RabbitMQ,Kafaka 等),可以採用不同的程式語言,使用不同的儲存技術,自動化部署(如 Jenkins)減少人為控制,降低出錯概率。服務數量越多,管理起來越複雜,因此採用集中化管理。例如 Eureka,Zookeeper 等都是比較常見的服務集中化管理框架。
**微服務是一種架構風格,架構就是為了解耦,實際使用的是分散式系統開發。**一個大型的複雜軟體應用,由一個或多個微服務組成。系統中的各個微服務可被獨立部署,各個微服務之間是鬆耦合的。每個微服務僅關注於完成一件任務並很好的完成該任務。
❝「一句話總結:微服務是 SOA 發展出來的產物,它是一種比較現代化的細粒度的 SOA 實現方式。」
❞
通訊方式
隨著網際網路的發展,應用程式從單機走向分散式,通訊方式也產生了很多的變化。
TCP/UDP
都是傳輸協議,主要區別是 TCP 協議連線需要 3 次握手,斷開需要四次揮手,是通過流來傳輸的,就是確定連線後,一直髮送資訊,傳完後斷開。UDP 不需要進行連線,直接把資訊封裝成多個報文,直接傳送。所以 UDP 的速度更快寫,但是不保證資料的完整性。
❝一句話總結:最古老且最有效,永不過時,學習成本高。所有通訊方式歸根結底都是 TCP/UDP。
❞
WebService
WebService(SOA,SOAP,WSDL,UDDI,XML)技術, 能使得執行在不同機器上的不同應用無須藉助附加的、專門的第三方軟體或硬體, 就可相互交換資料或整合。依據 WebService 規範實施的應用之間, 無論它們所使用的語言、 平臺或內部協議是什麼, 都可以相互交換資料。
WebService 就是一種跨程式語言和跨作業系統平臺的遠端呼叫技術。WebService 互動的過程就是遵循 SOAP 協議通過 XML 封裝資料,然後由 Http 協議來傳輸資料。
❝一句話總結:基於 HTTP + XML 的標準化 Web API。
❞
RESTful
Representational State Transfer,表現層狀態轉移。網際網路通訊協議 HTTP 協議,是一個無狀態協議。這意味著,所有的狀態都儲存在伺服器端。因此,如果客戶端想要操作伺服器,必須通過某種手段,讓伺服器端發生"狀態轉化"(State Transfer)。而這種轉化是建立在表現層之上的,所以就是"表現層狀態轉移"。
客戶端用到的手段,只能是 HTTP 協議。具體來說,就是 HTTP 協議裡面,四個表示操作方式的動詞:GET、POST、PUT、DELETE。它們分別對應四種基本操作:GET 用來獲取資源,POST 用來新建資源(也可以用於更新資源),PUT 用來更新資源,DELETE 用來刪除資源。
無狀態協議 HTTP,具備先天優勢,擴充套件能力很強。例如需要安全加密時,有現成的成熟方案 HTTPS 可用。 JSON 報文序列化,輕量簡單,人與機器均可讀,學習成本低,搜尋引擎友好。 語言無關,各大熱門語言都提供成熟的 Restful API 框架。
❝一句話總結:基於 HTTP + JSON 的標準化 Web API。
❞
RMI
Remote Method Invocation,遠端方法呼叫。Java 中實現的分散式通訊協議,它大大增強了 Java 開發分散式應用的能力。通過 RMI 技術,某一個本地的 JVM 可以呼叫存在於另外一個 JVM 中的物件方法,就好像它僅僅是在呼叫本地 JVM 中某個物件方法一樣。
❝一句話總結:基於 Java 語言的分散式通訊協議。
❞
JMS
Java Message Service,Java 訊息服務應用程式介面,是一個 Java 平臺中關於面向訊息中介軟體的 API,用於在兩個應用程式之間,或分散式系統中傳送訊息,進行非同步通訊。絕大多數 MQ 都對 JMS 提供支援,如 RabbitMQ、ActiveMQ、Kafka、RocketMQ 以及 Redis 等。
❝一句話總結:JavaEE 訊息框架標準。
❞
RPC
Remont Proceduce Call,遠端過程呼叫。它是一種通過網路從遠端計算機程式上請求服務,而不需要了解底層網路技術的思想。RPC 只是一個概念,它不是一個協議也不是一個框架。
RPC 的具體實現可以使用 RMI 或 RESTful 等,但一般不用,因為 RMI 不能跨語言,RESTful 效率太低。
RPC 多用於伺服器叢集內部通訊,因此常使用更加高效、短小精悍的傳輸模式以提高效率。RPC 框架有很多:Apache Thrift、Apache Dubbo、Google Grpc 等。
❝一句話總結:解決分散式系統中,服務之間的呼叫問題。遠端呼叫時,要能夠像本地呼叫一樣方便,讓呼叫者感知不到遠端呼叫的邏輯。
❞
為什麼需要 RPC?
主要就是因為在幾個程序內(應用分佈在不同的機器上),無法共用記憶體空間,比如不同的系統之間的通訊,甚至不同組織之間的通訊。此外由於機器的橫向擴充套件,需要在多臺機器組成的叢集上部署應用等等。
比如現在有兩臺機器:A 機器和 B 機器,並且分別部署了應用 A 和應用 B。假設此時位於 A 機器上的 A 應用想要呼叫位於 B 機器上的 B 應用提供的函式或是方法,由於 A 應用和 B 應用不在一個記憶體空間裡面,所以不能直接呼叫,此時就需要通過網路來表達呼叫的方式和傳輸呼叫的資料。也即所謂的遠端呼叫。
RPC 實現原理
一次完整的 RPC 呼叫流程包含了四個核心部分,分別是 Client
,Server
,Client Stub
以及 Server Stub
,這個 Stub 大家可以理解為存根。分別說說這幾個部分:
「客戶端(Client)」:服務的呼叫方。
「服務端(Server)」:服務的提供方。
「客戶端存根」:存放服務端的地址訊息,再將客戶端的請求引數打包成網路訊息,然後通過網路遠端傳送給服務方。
「服務端存根」:接收客戶端傳送過來的訊息,將訊息解包,並呼叫本地的方法。
客戶端(Client)以本地呼叫方式(即以介面的方式)呼叫服務; 客戶端存根(Client Stub)接收到呼叫後,負責將方法、引數等組裝成能夠進行網路傳輸的訊息體(將訊息體物件序列化為二進位制); 客戶端通過 Socket 將訊息傳送到服務端; 服務端存根(Server Stub)收到訊息後進行解碼(將訊息物件反序列化); 服務端存根(Server Stub)根據解碼結果呼叫本地的服務; 本地服務進行業務邏輯處理; 本地服務將業務邏輯處理後的結果返回給服務端存根(Server Stub); 服務端存根(Server Stub)將返回結果打包成訊息(將結果訊息物件序列化); 服務端(Server)通過 Socket 將訊息傳送到客戶端; 客戶端存根(Client Stub)接收到結果訊息,並進行解碼(將結果訊息反序列化); 客戶端(Client)得到最終結果。
❝RPC 的目標是要把 2、3、4、5、7、8、9、10 這些步驟都封裝起來。
❞
建立通訊
解決通訊的問題,主要是通過在客戶端和伺服器之間建立 TCP 連線,遠端過程呼叫的所有交換的資料都在這個連線裡傳輸。連線可以是按需連線,呼叫結束後就斷掉,也可以是長連線,多個遠端過程呼叫共享同一個連線。
服務定址
A 伺服器上的應用怎麼告訴底層的 RPC 框架,如何連線到 B 伺服器(如主機或 IP 地址)以及特定的埠,方法的名稱是什麼,這樣才能完成呼叫。比如基於 Web 服務協議棧的 RPC,就要提供一個 endpoint URI,或者是從 UDDI(一種目錄服務,通過該目錄服務進行服務註冊與搜尋)服務上查詢。如果是 RMI 呼叫的話,還需要一個 RMI Registry 來註冊服務的地址。
網路傳輸
序列化
A 伺服器上的應用發起遠端過程呼叫時,方法的引數需要通過底層的網路協議如 TCP 傳遞到 B 伺服器,由於網路協議是基於二進位制的,記憶體中的引數的值要序列化成二進位制的形式,也就是序列化(Serialize)或編組(marshal),通過定址和傳輸將序列化的二進位制傳送給 B 伺服器。
反序列化
B 伺服器收到請求後,需要對引數進行反序列化(序列化的逆操作),恢復為記憶體中的表達方式,然後再找到對應的方法(定址的一部分)進行本地呼叫(一般是通過生成代理 Proxy 去呼叫,通常會有 JDK 動態代理、CGLIB 動態代理、Javassist 生成位元組碼技術等),之後得到呼叫的返回值。
服務呼叫
B 機器進行本地呼叫(通過代理 Proxy)之後得到了返回值,此時還需要再把返回值傳送回 A 機器,同樣也需要經過序列化操作,然後再經過網路傳輸將二進位制資料傳送回 A 機器,而當 A 機器接收到這些返回值之後,則再次進行反序列化操作,恢復為記憶體中的表達方式,最後再交給 A 機器上的應用進行相關處理(一般是業務邏輯處理操作)。
RPC 基於 RMI 的簡單實現
搭建 Maven 聚合專案
父工程 rmi-demo
pom.xml
<?xmlversion="1.0"encoding="UTF-8"?>
<projectxmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>rmi-demo</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>rmi-api</module>
<module>rmi-server</module>
<module>rmi-client</module>
</modules>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
</project>
子工程 rmi-api
這個工程主要是存放 client 和 server 都會用到的公共介面。
pom.xml
<?xmlversion="1.0"encoding="UTF-8"?>
<projectxmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>rmi-demo</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>rmi-api</artifactId>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
</dependencies>
</project>
實體類
由於網路協議是基於二進位制的,記憶體中的引數的值要序列化成二進位制的形式,所以實體類需要實現 Serializable
介面。
packageorg.example.pojo;
importlombok.AllArgsConstructor;
importlombok.Data;
importlombok.NoArgsConstructor;
importjava.io.Serializable;
@Data
@AllArgsConstructor
@NoArgsConstructor
publicclassUserimplementsSerializable{
privatestaticfinallongserialVersionUID=2159427410483687648L;
privateIntegerid;
privateStringusername;
}
服務介面
使用 JavaRMI 對外暴露的服務介面必須繼承 java.rmi.Remote.Remote
類,方法必須丟擲 java.rmi.RemoteException
異常。
packageorg.example.service;
importorg.example.pojo.User;
importjava.rmi.Remote;
importjava.rmi.RemoteException;
/**
*使用者管理服務
*/
publicinterfaceUserServiceextendsRemote{
UserselectUserById(IntegeruserId)throwsRemoteException;
}
子工程 rmi-server
主要提供服務介面的實現以及 RMI 的服務配置。
pom.xml
<?xmlversion="1.0"encoding="UTF-8"?>
<projectxmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>rmi-demo</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>rmi-server</artifactId>
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>rmi-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
服務實現
服務介面實現必須繼承 java.rmi.server.UnicastRemoteObject
類。
packageorg.example.service.impl;
importorg.example.pojo.User;
importorg.example.service.UserService;
importjava.rmi.RemoteException;
importjava.rmi.server.UnicastRemoteObject;
/**
*使用者管理服務
*/
publicclassUserServiceImplextendsUnicastRemoteObjectimplementsUserService{
publicUserServiceImpl()throwsRemoteException{
}
@Override
publicUserselectUserById(IntegeruserId)throwsRemoteException{
System.out.println("使用者管理服務接收到客戶端請求,請求引數userId="+userId);
//模擬假資料返回
returnnewUser(userId,"張三");
}
}
釋出服務
將服務釋出在指定的 IP + 埠上。
packageorg.example;
importorg.example.service.UserService;
importorg.example.service.impl.UserServiceImpl;
importjava.net.MalformedURLException;
importjava.rmi.AlreadyBoundException;
importjava.rmi.Naming;
importjava.rmi.RemoteException;
importjava.rmi.registry.LocateRegistry;
/**
*釋出服務
*/
publicclassPublish{
publicstaticvoidmain(String[]args)throwsRemoteException{
UserServiceuserService=newUserServiceImpl();
try{
//對外暴露的服務埠
LocateRegistry.createRegistry(8888);
//對外暴露的服務地址
Naming.bind("rmi://localhost:8888/userService",userService);
System.out.println("服務釋出成功!");
}catch(AlreadyBoundExceptione){
e.printStackTrace();
}catch(MalformedURLExceptione){
e.printStackTrace();
}
}
}
子工程 rmi-client
本地 client 如何實現呼叫遠端介面。
pom.xml
<?xmlversion="1.0"encoding="UTF-8"?>
<projectxmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>rmi-demo</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>rmi-client</artifactId>
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>rmi-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
遠端服務呼叫
通過指定的 IP + 埠進行遠端服務呼叫。
packageorg.example.controller;
importorg.example.pojo.User;
importorg.example.service.UserService;
importjava.net.MalformedURLException;
importjava.rmi.Naming;
importjava.rmi.NotBoundException;
importjava.rmi.RemoteException;
publicclassUserController{
publicstaticvoidmain(String[]args){
try{
UserServiceuserService=(UserService)Naming.lookup("rmi://localhost:8888/userService");
Useruser=userService.selectUserById(1);
System.out.println("遠端服務呼叫成功,返回值資訊為:"+user);
}catch(NotBoundExceptione){
e.printStackTrace();
}catch(MalformedURLExceptione){
e.printStackTrace();
}catch(RemoteExceptione){
e.printStackTrace();
}
}
}
測試
通過測試可以看到 RPC 基於 RMI 的遠端服務呼叫已經完成,接下來我們一起學習一下如何使用 RPC 框架 Dubbo 來完成遠端服務呼叫。
RPC 框架
一個典型 RPC 框架使用場景中,包含了服務註冊與發現(註冊中心)、負載均衡、容錯、網路傳輸、序列化等元件,其中“RPC 相關協議”就指明瞭程式如何進行網路傳輸和序列化。RPC 框架有很多:Apache Thrift、Apache Dubbo、Google Grpc 等。下圖為完整的 RPC 框架架構圖:
Dubbo 介紹
官網:http://dubbo.apache.org/zh-cn/
Github:https://github.com/apache/dubbo
2018 年 2 月 15 日,阿里巴巴的服務治理框架 dubbo 通過投票,順利成為 Apache 基金會孵化專案。
Apache Dubbo 是一款高效能、輕量級的開源 Java RPC 框架,它提供了三大核心能力:面向介面的遠端方法呼叫
,智慧容錯和負載均衡
,以及服務自動註冊和發現
。
Dubbo 架構
Dubbo 提供三個核心功能:面向介面的遠端方法呼叫
、智慧容錯和負載均衡
,以及服務自動註冊和發現
。Dubbo 框架廣泛的在阿里巴巴內部使用,以及噹噹、去哪兒、網易考拉、滴滴等都在使用。
節點角色說明
節點 | 角色說明 |
---|---|
Provider |
暴露服務的服務提供方 |
Consumer |
呼叫遠端服務的服務消費方 |
Registry |
服務註冊與發現的註冊中心 |
Monitor |
統計服務的呼叫次數和呼叫時間的監控中心 |
Container |
服務執行容器 |
呼叫關係說明
服務容器負責啟動,載入,執行服務提供者。 服務提供者在啟動時,向註冊中心註冊自己提供的服務。 服務消費者在啟動時,向註冊中心訂閱自己所需的服務。 註冊中心返回服務提供者地址列表給消費者,如果有變更,註冊中心將基於長連線推送變更資料給消費者。 服務消費者,從提供者地址列表中,基於軟負載均衡演算法,選一臺提供者進行呼叫,如果呼叫失敗,再選另一臺呼叫。 服務消費者和提供者,在記憶體中累計呼叫次數和呼叫時間,定時每分鐘傳送一次統計資料到監控中心。
Dubbo 快速入門
下面我們基於 SpringBoot 環境整合 Dubbo 完成一個入門案例。
搭建 Maven 聚合專案
父工程 dubbo-demo
pom.xml
<?xmlversion="1.0"encoding="UTF-8"?>
<projectxmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>dubbo-demo</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>dubbo-api</module>
<module>dubbo-provider</module>
<module>dubbo-consumer</module>
</modules>
<!--繼承spring-boot-starter-parent依賴-->
<!--使用繼承方式,實現複用,符合繼承的都可以被使用-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.4.1</version>
</dependency>
</dependencies>
</project>
子工程 dubbo-api
這個工程主要是存放 provider 和 consumer 都會用到的公共介面。
pom.xml
<?xmlversion="1.0"encoding="UTF-8"?>
<projectxmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>dubbo-demo</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>dubbo-api</artifactId>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
實體類
由於網路協議是基於二進位制的,記憶體中的引數的值要序列化成二進位制的形式,所以實體類需要實現 Serializable
介面。
packageorg.example.pojo;
importlombok.AllArgsConstructor;
importlombok.Data;
importlombok.NoArgsConstructor;
importjava.io.Serializable;
@Data
@AllArgsConstructor
@NoArgsConstructor
publicclassUserimplementsSerializable{
privatestaticfinallongserialVersionUID=2159427410483687648L;
privateIntegerid;
privateStringusername;
}
服務介面
packageorg.example.service;
importorg.example.pojo.User;
/**
*使用者管理服務
*/
publicinterfaceUserService{
UserselectUserById(IntegeruserId);
}
子工程 dubbo-provider
主要提供服務介面的實現以及服務提供者的服務配置。
pom.xml
<?xmlversion="1.0"encoding="UTF-8"?>
<projectxmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>dubbo-demo</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>dubbo-provider</artifactId>
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>dubbo-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
配置檔案
配置檔案需要配置服務提供者應用資訊,配置註冊中心及暴露服務的地址,還有采用什麼樣的協議及協議埠和掃描服務介面的包地址。
server:
port:7070#埠
#Dubbo配置
dubbo:
#提供方應用資訊,用於計算依賴關係
application:
name:product-service
#配置註冊中心
registry:
protocol:zookeeper#使用ZooKeeper註冊中心暴露服務地址
address:192.168.10.101:2181,192.168.10.102:2181,192.168.10.103:2181
#配置元資料中心
metadata-report:
address:zookeeper://192.168.10.101:2181?backup=192.168.10.102:2181,192.168.10.103:2181
config-center:
timeout:10000#連線註冊中心的超時時間,單位毫秒,預設3000
#用dubbo協議在20880埠暴露服務
protocol:
name:dubbo#協議名稱
port:20880#協議埠
#掃描需要暴露的服務
scan:
base-packages:org.example.service
服務實現(服務提供者)
packageorg.example.service.impl;
importorg.example.pojo.User;
importorg.example.service.UserService;
importorg.apache.dubbo.config.annotation.Service;
/**
*使用者管理服務
*timeout呼叫該服務的超時時間
*version為版本號
*group為分組
*interface、group、version三者可確定一個服務
*parameters={"unicast","false"}
*建議服務提供者和服務消費者在不同機器上執行,
*如果在同一機器上,需設定unicast=false禁用單播訂閱,只有multicast註冊中心有此問題。
*/
@Service(timeout=5000,version="1.0",group="user-provider",parameters={"unicast","false"})
publicclassUserServiceImplimplementsUserService{
@Override
publicUserselectUserById(IntegeruserId){
System.out.println("使用者管理服務接收到客戶端請求,請求引數userId="+userId);
//模擬假資料返回
returnnewUser(userId,"張三");
}
}
❝注意:
❞parameters = {"unicast", "false"}
:建議服務提供者和服務消費者在不同機器上執行,如果在同一機器上,需設定 unicast = false 禁用單播訂閱,只有 multicast 註冊中心有此問題。
啟動類
packageorg.example;
importorg.springframework.boot.SpringApplication;
importorg.springframework.boot.autoconfigure.SpringBootApplication;
//掃描需要暴露的服務,如果配置檔案中已宣告則無需新增該註解
//@EnableDubbo(scanBasePackages="org.example.service")
@SpringBootApplication
publicclassDubboProviderApplication{
publicstaticvoidmain(String[]args){
SpringApplication.run(DubboProviderApplication.class,args);
}
}
子工程 dubbo-consumer
pom.xml
<?xmlversion="1.0"encoding="UTF-8"?>
<projectxmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>dubbo-demo</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>dubbo-consumer</artifactId>
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>dubbo-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
配置檔案
配置檔案需要配置服務消費者應用資訊,配置註冊中心用於發現註冊中心暴露的服務。
server:
port:9090#埠
#Dubbo配置
dubbo:
#消費方應用名,用於計算依賴關係,不是匹配條件,不要與提供方一樣
application:
name:dubbo-consumer
#配置註冊中心
registry:
address:multicast://224.5.6.7:1234#發現Multicast註冊中心暴露的服務
遠端服務呼叫
packageorg.example.controller;
importorg.apache.dubbo.config.annotation.Reference;
importorg.example.pojo.User;
importorg.example.service.UserService;
importorg.springframework.web.bind.annotation.GetMapping;
importorg.springframework.web.bind.annotation.PathVariable;
importorg.springframework.web.bind.annotation.RequestMapping;
importorg.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/consumer")
publicclassConsumerController{
//dubbo提供了@Reference註解,可替換@Autowired註解,用於引入遠端服務
//如果註冊服務時設定了版本及分組資訊,呼叫遠端服務時也要設定對應的版本及分組資訊
@Reference(timeout=5000,version="1.0",group="user-provider",parameters={"unicast","false"})
privateUserServiceuserService;
@GetMapping("/{id}")
publicUserselectUserById(@PathVariable("id")Integerid){
returnuserService.selectUserById(id);
}
}
啟動類
packageorg.example;
importorg.springframework.boot.SpringApplication;
importorg.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
publicclassDubboConsumerApplication{
publicstaticvoidmain(String[]args){
SpringApplication.run(DubboConsumerApplication.class,args);
}
}
測試
啟動服務提供者和服務消費者,訪問:http://localhost:9090/consumer/1 結果如下:
Dubbo 常用標籤
dubbo:application
:應用程式名稱dubbo:registry
:連線註冊中心資訊(配置註冊中心)dubbo:protocol
:服務提供者註冊服務採用的協議Dubbo 協議,預設 RMI 協議 Hessian 協議 HTTP 協議 WebService 協議 Thrift 協議 Memcached 協議 Redis 協議 Rest 協議(RESTful) Grpc 協議 更多協議資訊請參考:http://dubbo.apache.org/zh-cn/docs/user/references/protocol/introduction.html
dubbo:service
:宣告需要暴露的服務介面dubbo:reference
:配置訂閱的服務(生成遠端服務代理)
更多配置資訊請參考:http://dubbo.apache.org/zh-cn/docs/user/references/xml/introduction.html
❝下一篇我們講解 Dubbo 支援的註冊中心,Dubbo 負載均衡策略和 Dubbo 控制檯的安裝,記得關注噢~
❞
本文采用 知識共享「署名-非商業性使用-禁止演繹 4.0 國際」許可協議
。