微服務(wù)體系中的分層設(shè)計(jì)和領(lǐng)域劃分!
引言
看標(biāo)題感覺這個(gè)東西很理論,比起“高并發(fā)、多線程”、“分布式CAP、一致性、Paxos”、“高可用SLA”等具體的干貨技術(shù)點(diǎn),軟件體系知識(shí)顯得很“濕”,似乎人人都有自己的認(rèn)識(shí),但又很少有人能說(shuō)完整,有一點(diǎn)可以確定的是,如果你未來(lái)需要獨(dú)立設(shè)計(jì)一個(gè)復(fù)雜的系統(tǒng)中臺(tái),并使之未來(lái)能快速應(yīng)對(duì)各種需求變化的話,科學(xué)合理的領(lǐng)域劃分和邊界界定需要我們“處女座級(jí)”的堅(jiān)持下去,這對(duì)防止人力失控、減少項(xiàng)目爛尾很有幫助。合理的界定了邊界后,即便某個(gè)微服務(wù)很糟糕,也可以就輸入輸出以很少的人力投入進(jìn)行重構(gòu),相反的就是牽一發(fā)而動(dòng)全身,加上業(yè)務(wù)需求頻繁而來(lái),很容易爛尾或是達(dá)不到如期的效果。
其實(shí)很多技術(shù)大神都是某一個(gè)技術(shù)點(diǎn)的好手,但可能在整體軟件體系上思考并不多,每個(gè)人都有自己的設(shè)計(jì)方法,大部分容易想到的設(shè)計(jì)方法處理一般的系統(tǒng)已經(jīng)夠了,后面發(fā)生問(wèn)題慢慢打補(bǔ)丁就行了,當(dāng)我們面對(duì)各種需求變化陷入開發(fā)困境的時(shí)候我們就該想想了,咱們系統(tǒng)的體系設(shè)計(jì)上是否出了問(wèn)題?
本文不打算涉及領(lǐng)域建模和設(shè)計(jì)模式等代碼級(jí)別的詳述,而是探討如何將一個(gè)復(fù)雜的大系統(tǒng)進(jìn)行分層和拆分,這是設(shè)計(jì)一個(gè)優(yōu)美系統(tǒng)的第一步,相信對(duì)各BU同事們快速搭建系統(tǒng)中臺(tái)也是很有參考意義的。文中的一些例子大家也可能遇到過(guò),大家如果在開發(fā)中遇到困境,可以多來(lái)圈子交流和發(fā)表問(wèn)題,大家一起學(xué)習(xí)進(jìn)步。大概知道內(nèi)容背景的可以直接跳到第3部分。想了解一個(gè)大項(xiàng)目如何進(jìn)行科學(xué)人員安排的可以直接看5.4部分。如果你的組里還有人把數(shù)據(jù)庫(kù)模型當(dāng)接口契約用,可以建議他看下5.1部分。假如你在開發(fā)過(guò)程中遇到一些別人的開發(fā)設(shè)計(jì)習(xí)慣,你覺得不是很好,但是又不知道如何說(shuō)服他,都可以到評(píng)論區(qū)聊聊,大家一起討論討論。
1.摘要
本文闡述了一種將分層設(shè)計(jì)和DDD領(lǐng)域設(shè)計(jì)思想應(yīng)用于微服務(wù)體系架構(gòu)的方案實(shí)踐,也是個(gè)人的最佳實(shí)踐。對(duì)于大部分互聯(lián)網(wǎng)公司來(lái)說(shuō),我們主張將其Web服務(wù)架構(gòu)分為五層:基礎(chǔ)設(shè)施層、領(lǐng)域服務(wù)層、應(yīng)用服務(wù)層、網(wǎng)關(guān)層和用戶界面層(表示層)。
領(lǐng)域服務(wù)層和應(yīng)用服務(wù)層均可以采用微服務(wù)設(shè)計(jì)進(jìn)行拆分,其中領(lǐng)域服務(wù)層將按照DDD領(lǐng)域設(shè)計(jì)進(jìn)行領(lǐng)域劃分,設(shè)計(jì)為一個(gè)個(gè)領(lǐng)域模塊微服務(wù),每個(gè)微服務(wù)高度內(nèi)聚,僅關(guān)注自己的業(yè)務(wù),領(lǐng)域服務(wù)間通過(guò)接口調(diào)用進(jìn)行松耦合。這種設(shè)計(jì)方案可以大大簡(jiǎn)化大系統(tǒng),并且在后期的維護(hù)中優(yōu)勢(shì)會(huì)日漸凸顯,然而把大系統(tǒng)分而治之拆成微服務(wù)同時(shí)也對(duì)架構(gòu)師和開發(fā)人員提出了更高的要求。
第2部分介紹了相關(guān)背景,接著第3部分探討了分層設(shè)計(jì)以及每一層的功能,第4部分結(jié)合微服務(wù)和DDD對(duì)領(lǐng)域服務(wù)層進(jìn)行服務(wù)模塊劃分和設(shè)計(jì)。第5部分則就分層設(shè)計(jì)和DDD領(lǐng)域設(shè)計(jì)中常見的問(wèn)題進(jìn)行了整理。
2.背景介紹
想寫這樣一篇文章很久了,雖然本科學(xué)的是軟件工程,但礙于自己能力有限,從08年寫代碼以來(lái)一直斷斷續(xù)續(xù)的思考,始終對(duì)項(xiàng)目模塊設(shè)計(jì)和分層結(jié)構(gòu)設(shè)計(jì)沒(méi)有一個(gè)可以讓自己覺得滿意且無(wú)糾結(jié)點(diǎn)的答案,假設(shè)了某個(gè)設(shè)計(jì),很快在實(shí)踐中又會(huì)發(fā)現(xiàn)其存在著一些問(wèn)題。直到2014年畢業(yè)工作了解了DDD領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)后,才有了相對(duì)清晰的方向。實(shí)際上早在2004年,Eric Envas的《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì):軟件核心復(fù)雜性應(yīng)對(duì)之道》就已出版,畢竟軟件開發(fā)自計(jì)算機(jī)普及以來(lái)已經(jīng)存在很長(zhǎng)一段時(shí)間了,早期國(guó)外程序員對(duì)軟件開發(fā)理論的研究也十分興盛,如今成熟后反而研究的相對(duì)少了,基本上依葫蘆畫瓢即可。
DDD領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)對(duì)軟件設(shè)計(jì)各個(gè)環(huán)節(jié)的人員都有較高的要求,用《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)》一書的話來(lái)說(shuō)它需要一個(gè)“領(lǐng)域驅(qū)動(dòng)團(tuán)隊(duì)”[1],它要求從分析階段,產(chǎn)品經(jīng)理、項(xiàng)目經(jīng)理、架構(gòu)師以及開發(fā)工程師就使用統(tǒng)一的模型語(yǔ)言(Ubiquitous Language)來(lái)進(jìn)行溝通,并且他們都懂一些代碼、產(chǎn)品和建模相關(guān)的知識(shí),事實(shí)上這在國(guó)內(nèi)很難實(shí)施,國(guó)內(nèi)的產(chǎn)品經(jīng)理約等于需求整理工,對(duì)其計(jì)算機(jī)基礎(chǔ)的要求是少之又少,在我所從事的公司里,也曾發(fā)生過(guò)產(chǎn)品經(jīng)理直接指導(dǎo)開發(fā),以至于后面雙方理解的同一個(gè)詞有著不同含義的情況。所以本文不打算去闡述DDD領(lǐng)域內(nèi)部建模代碼級(jí)別的實(shí)踐,甚至本文并不認(rèn)為貧血模型是不好的,本文主要探討領(lǐng)域之間的劃分和分層設(shè)計(jì),正如引言說(shuō)提到的,這是設(shè)計(jì)優(yōu)美系統(tǒng)的第一步。另外提一句:其實(shí)合理設(shè)計(jì)的微服務(wù)體系中的服務(wù)本身就是功能單一邊界清晰的小應(yīng)用,屆時(shí)貧血也好、DDD領(lǐng)域建模也好,其實(shí)都可以勝任。
近年來(lái),隨著分布式的發(fā)展,傳統(tǒng)中小型機(jī)集中式服務(wù)器已經(jīng)不在流行,所以微服務(wù)體系也成為了各大互聯(lián)網(wǎng)公司主流的選擇。直觀的感受下微服務(wù)和DDD兩者,似乎一個(gè)是微系統(tǒng),另一個(gè)則是大系統(tǒng)的設(shè)計(jì)方法,似乎兩者天生互斥,微服務(wù)化的小系統(tǒng)也用不著DDD,其實(shí)并不是,DDD是針對(duì)整個(gè)復(fù)雜的軟件解決方案的一種科學(xué)設(shè)計(jì)方法,微服務(wù)化也是把復(fù)雜的大系統(tǒng)拆分為小系統(tǒng),方便維護(hù)和管理,所以兩者都有一個(gè)特點(diǎn)——為復(fù)雜的大系統(tǒng)服務(wù)。下面咱們就來(lái)探討下,如何把DDD的領(lǐng)域設(shè)計(jì)和其主張的分層設(shè)計(jì)應(yīng)用到微服務(wù)體系架構(gòu)中。需要說(shuō)明的是本文主要是個(gè)人多年來(lái)的一點(diǎn)總結(jié),未必適合所有場(chǎng)景,有更好通用性更為廣泛的方案請(qǐng)不吝賜教。
3.分層設(shè)計(jì)
準(zhǔn)確的說(shuō)分層設(shè)計(jì)(Layered Architecture)跟DDD沒(méi)有必然的聯(lián)系,我最早接觸分層設(shè)計(jì)是在攜程網(wǎng),當(dāng)時(shí)內(nèi)部使用的應(yīng)該只是簡(jiǎn)單的業(yè)務(wù)層(Biz)和表示層,數(shù)據(jù)庫(kù)訪問(wèn)之類的也是放在各自的業(yè)務(wù)包下的。后來(lái)接觸和學(xué)習(xí)了《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì):軟件核心復(fù)雜性應(yīng)對(duì)之道》,書的第4章“分離領(lǐng)域”中說(shuō)到了四層分層設(shè)計(jì),即:基礎(chǔ)設(shè)施層、領(lǐng)域?qū)印?yīng)用層和用戶界面層(表示層)。DDD產(chǎn)生的年代微服務(wù)還未流行,當(dāng)時(shí)甚至基于瀏覽器的Web應(yīng)用都比較少,更多的是PC軟件和EJB等網(wǎng)絡(luò)應(yīng)用,所以作者更多的是想表達(dá)對(duì)復(fù)雜系統(tǒng)的邏輯分層,并不在意每個(gè)領(lǐng)域是單獨(dú)的系統(tǒng)還是一個(gè)軟件系統(tǒng)內(nèi)不同的模塊。
所以為了跟其做區(qū)分,我們建議的四層為在其基礎(chǔ)上引入“服務(wù)”兩個(gè)字,即:基礎(chǔ)設(shè)施層、領(lǐng)域服務(wù)層、應(yīng)用服務(wù)層和用戶界面層。這樣做的意圖是讓開發(fā)人員立刻可以了解到——每個(gè)領(lǐng)域模塊即一個(gè)微服務(wù)(一個(gè)領(lǐng)域可以對(duì)應(yīng)一個(gè)或者多個(gè)模塊Module)。摘要中提到我們主張的分層體系中還有一個(gè)層,即網(wǎng)關(guān)層,這又是什么鬼呢。剛剛提到的DDD的時(shí)代背景,PC軟件系統(tǒng)或者企業(yè)內(nèi)部使用的網(wǎng)絡(luò)應(yīng)用系統(tǒng)是根本沒(méi)有網(wǎng)關(guān)層(有也是網(wǎng)絡(luò)網(wǎng)關(guān)設(shè)備)這一說(shuō)的,而現(xiàn)如今互聯(lián)網(wǎng)公司產(chǎn)品的輸出形式無(wú)外乎Web應(yīng)用(網(wǎng)站、或者網(wǎng)絡(luò)服務(wù)),并且為了更好的適配PC站和App,一般會(huì)采用前后端分離的應(yīng)用設(shè)計(jì)方案,這時(shí)候會(huì)產(chǎn)生一個(gè)需求——內(nèi)部網(wǎng)絡(luò)應(yīng)用系統(tǒng)如何把自己的服務(wù)輸出到互聯(lián)網(wǎng)上,供外部系統(tǒng)或者瀏覽器網(wǎng)頁(yè)訪問(wèn)。最直接的方式就是把應(yīng)用層直接暴露在公網(wǎng)上,但我們不建議這么做,應(yīng)用層服務(wù)更多的是關(guān)注業(yè)務(wù)應(yīng)用,對(duì)網(wǎng)絡(luò)級(jí)的系統(tǒng)安全性(防DDOS、釣魚、跨域等)、請(qǐng)求監(jiān)控等缺乏考慮,這些工作交給網(wǎng)關(guān)層統(tǒng)一管理會(huì)輕松很多(比如淘寶的TOP平臺(tái))。
這時(shí)候我們?cè)赪eb應(yīng)用系統(tǒng)中引入網(wǎng)關(guān)層用于銜接表示層和應(yīng)用層 ,因?yàn)檫@樣可以更好的劃分各層的職能。網(wǎng)關(guān)層也可以看作是應(yīng)用服務(wù)層的對(duì)外包裝層。如果一定要把網(wǎng)關(guān)層做到應(yīng)用服務(wù)層里理論上也是可行的,比如針對(duì)于Spring Cloud這種框架下的微服務(wù)體系,可以考慮直接暴露應(yīng)用層,只需輔助一些運(yùn)維手段進(jìn)行統(tǒng)一的安全驗(yàn)證和監(jiān)控即可。假設(shè)我們選擇引入網(wǎng)關(guān)層,那么我們就得到了以下網(wǎng)絡(luò)應(yīng)用系統(tǒng)分層體系:
其中,各層的職能和作用為[2]:
- 用戶界面層:負(fù)責(zé)向用戶顯示和解釋用戶指令。這里指的用戶可以是另一個(gè)計(jì)算機(jī)系統(tǒng),不一定是使用用戶界面的人(比如外部應(yīng)用調(diào)用對(duì)應(yīng)接口)。在全系統(tǒng)的視角看,通常情況下,這里指的是前端,但實(shí)際上,隨著前端的日益復(fù)雜,前端也可以分為多層,那種情況另當(dāng)別論。
- 網(wǎng)關(guān)層:負(fù)責(zé)提供對(duì)外的HTTP服務(wù)或者其他網(wǎng)絡(luò)應(yīng)用層協(xié)議(這里是指OSI七層協(xié)議中的應(yīng)用層,別混淆了哦)服務(wù)。該層從非業(yè)務(wù)邏輯角度對(duì)暴露到外部的接口進(jìn)行鑒權(quán)、計(jì)費(fèi)、風(fēng)控、資源調(diào)配等操作。有些簡(jiǎn)易的系統(tǒng)中,改成可以合并入應(yīng)用服務(wù)層中作為一個(gè)AOP存在。
- 應(yīng)用服務(wù)層:定義軟件要完成的任務(wù),并且指揮表達(dá)領(lǐng)域概念的對(duì)象來(lái)解決問(wèn)題。這一層所負(fù)責(zé)的工作對(duì)業(yè)務(wù)來(lái)說(shuō)意義重大,也是與其他系統(tǒng)的應(yīng)用層進(jìn)行交互的必要渠道。應(yīng)用層要盡量簡(jiǎn)單,不包含業(yè)務(wù)規(guī)則或者知識(shí),而只為下一層中的領(lǐng)域?qū)ο髤f(xié)調(diào)任務(wù),分配工作,使他們互相協(xié)作。它沒(méi)有反應(yīng)業(yè)務(wù)情況的狀態(tài),但是卻可以具有另外一種狀態(tài),為用戶或者程序顯示某個(gè)任務(wù)的進(jìn)度。
- 領(lǐng)域服務(wù)層:負(fù)責(zé)表達(dá)業(yè)務(wù)概念,業(yè)務(wù)狀態(tài)信息以及業(yè)務(wù)規(guī)則。盡管保存業(yè)務(wù)狀態(tài)的技術(shù)細(xì)節(jié)是由基礎(chǔ)設(shè)施層實(shí)現(xiàn)的,但是反應(yīng)業(yè)務(wù)情況的狀態(tài)是由本層控制并且使用的。領(lǐng)域?qū)邮菢I(yè)務(wù)軟件的核心。
- 基礎(chǔ)設(shè)施層:為上面各層提供通用的技術(shù)能力,為應(yīng)用層傳遞消息,為領(lǐng)域?qū)犹峁┏志没瘷C(jī)制,為用戶界面層繪制屏幕組件(PS:這個(gè)在互聯(lián)網(wǎng)應(yīng)用中幾乎用不到)等等?;ヂ?lián)網(wǎng)Web應(yīng)用系統(tǒng)中基礎(chǔ)設(shè)施包含了數(shù)據(jù)持久化服務(wù),中間件服務(wù)(數(shù)據(jù)庫(kù),Redis,Memcached,zookeeper,ELK等等)以及第三方服務(wù)等。
各層除了實(shí)現(xiàn)自己的功能外,還需要遵守以下原則:
- 每一層設(shè)計(jì)保持內(nèi)聚,并且只依賴于它的下方的層。
- 下層向上層發(fā)起的通信只能通過(guò)中間件等間接方式進(jìn)行。[2]
- 上層和下層只能有松散耦合(各自為獨(dú)立個(gè)體,通過(guò)簡(jiǎn)單引用關(guān)聯(lián))。在某些微服務(wù)框架比如Dubbo中,可以把a(bǔ)pi包提供給上層引用即可。這也符合依賴倒置原則。
這里重點(diǎn)說(shuō)明應(yīng)用服務(wù)層和領(lǐng)域服務(wù)層之間的關(guān)系。舉一個(gè)我經(jīng)常跟部門其他開發(fā)舉的一個(gè)例子:有一家上市企業(yè)A公司,靠賣水果發(fā)家,其首席架構(gòu)師科學(xué)合理的按照DDD搭建了一套基于微服務(wù)體系的賣水果應(yīng)用,其架構(gòu)圖如下:
今年水果行情一般,而房地產(chǎn)十分火熱,A公司高層發(fā)現(xiàn)房地產(chǎn)帶動(dòng)的五金行業(yè)也十分火熱,于是下達(dá)任務(wù)給技術(shù)部,要求其立即著手搭建五金銷售系統(tǒng),貨源已經(jīng)談好。得益于首席架構(gòu)師之前優(yōu)秀的架構(gòu)設(shè)計(jì),他發(fā)現(xiàn)只需要做一個(gè)賣五金的網(wǎng)站以及另外對(duì)微服務(wù)進(jìn)行微量的調(diào)整即可滿足老板的需求——因?yàn)橘u五金和賣水果并無(wú)本質(zhì)區(qū)別,他們涉及的環(huán)節(jié)幾乎一致。加入五金售賣的系統(tǒng)架構(gòu)圖如下:
可見應(yīng)用服務(wù)層代表是某一個(gè)業(yè)務(wù)應(yīng)用,它代表的更多的是從需求出發(fā)的應(yīng)用定義,而領(lǐng)域服務(wù)層則是業(yè)務(wù)領(lǐng)域按照自身的邊界進(jìn)行設(shè)計(jì)的一個(gè)高內(nèi)聚的服務(wù)體。應(yīng)用層通過(guò)協(xié)調(diào)和組合各個(gè)領(lǐng)域服務(wù)即可形成一個(gè)新的應(yīng)用服務(wù)?!额I(lǐng)域驅(qū)動(dòng)設(shè)計(jì)》中明確指出,在設(shè)計(jì)領(lǐng)域服務(wù)時(shí)無(wú)需考慮表示層和持久層服務(wù)的東西。我在現(xiàn)實(shí)開發(fā)中總是遇到大量工程師按照產(chǎn)品的設(shè)計(jì)稿一溜煙的從上至下設(shè)計(jì)應(yīng)用層服務(wù)和領(lǐng)域?qū)臃?wù),完全沒(méi)有考慮業(yè)務(wù)領(lǐng)域的概念,導(dǎo)致后面微服務(wù)數(shù)量膨脹,功能重復(fù)度高。這種開發(fā)習(xí)慣代表的是《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)》作者極力吐槽的一種模式——SMART UI “反模式”[5]。
4.領(lǐng)域劃分和微服務(wù)化
根據(jù)DDD理論,領(lǐng)域建模主要發(fā)生在領(lǐng)域服務(wù)層,各領(lǐng)域模塊都應(yīng)該是高內(nèi)聚低耦合的,具有清晰的業(yè)務(wù)邊界。本文不打算討論具體的DDD建模(服務(wù),工廠,倉(cāng)庫(kù),實(shí)體,值對(duì)象,聚合等),這需要對(duì)DDD有較深入的研究,就目前所從事過(guò)的公司來(lái)看,似乎沒(méi)有一家真正嚴(yán)格按照DDD進(jìn)行項(xiàng)目代碼設(shè)計(jì)的,就像摘要中說(shuō)的,這對(duì)整個(gè)軟件工程鏈路上的人員都有較高的要求。有機(jī)會(huì)可以單獨(dú)寫一篇關(guān)于自己對(duì)DDD建模的思考和建議,本文更多的是討論高視角下的領(lǐng)域服務(wù)拆分,從而搭建一個(gè)低耦合高內(nèi)聚的微服務(wù)體系。如果一定要將微服務(wù)和DDD聯(lián)系起來(lái)的話,領(lǐng)域?qū)拥奈⒎?wù)就對(duì)應(yīng)了DDD中的領(lǐng)域模塊Module,每個(gè)Module由多個(gè)Service模式對(duì)象以及對(duì)應(yīng)的模型對(duì)象(實(shí)體, 值對(duì)象以及它們的聚合)組成。
從《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì):軟件核心復(fù)雜性應(yīng)對(duì)之道?!分形覍W(xué)到的主要有兩塊:領(lǐng)域設(shè)計(jì)思想和領(lǐng)域建模模式。本文更多的是對(duì)前者的運(yùn)用,后者的對(duì)立模式是貧血模型,大家日常用到的也都是貧血模型,我也覺得貧血模型有存在的必要性,所以本文我們主要從其中借鑒一下領(lǐng)域設(shè)計(jì)思想。本文所描述的設(shè)計(jì)理念,并不影響具體的模型設(shè)計(jì)方法,我們?nèi)匀豢梢栽诿總€(gè)微服務(wù)中使用DDD領(lǐng)域建模。
如何切分領(lǐng)域模塊并沒(méi)有一個(gè)明確的規(guī)則,不同的場(chǎng)景下可能相同的業(yè)務(wù)塊邊界也不盡相同。這里提幾點(diǎn)領(lǐng)域劃分的個(gè)人心得:
- 領(lǐng)域設(shè)計(jì)一定要有清晰的功能邊界。一個(gè)領(lǐng)域服務(wù)對(duì)應(yīng)了一個(gè)功能集合,這些功能一定是有一些共性的。比如,訂單服務(wù),那么創(chuàng)建訂單、修改訂單、查詢訂單列表,一般是訂單域的功能集合。
- 領(lǐng)域拆分并不是一步到位的,應(yīng)當(dāng)根據(jù)實(shí)際情況逐步展開。從單體應(yīng)用到微服務(wù)體系的拆分過(guò)程能很好的說(shuō)明這個(gè)問(wèn)題,一上來(lái)拆的很細(xì)的改造方案一定會(huì)死的很慘。所以如果一開始不知道應(yīng)該劃分多細(xì),完全可以先粗粒度劃分,然后隨著需要,初步拆分。比如一個(gè)電商一開始索性可以拆分為商品服務(wù)和交易服務(wù),一個(gè)負(fù)責(zé)展示商品,一個(gè)負(fù)責(zé)購(gòu)買支付。隨后隨著交易服務(wù)越來(lái)越復(fù)雜,就可以逐步的拆分成訂單服務(wù)和支付服務(wù)。
- 領(lǐng)域拆分并不是一成不變的,應(yīng)當(dāng)具體情況具體分析。2015年在大眾點(diǎn)評(píng)的時(shí)候,其訂單服務(wù)就拆分為了order-service和order-query-service,一來(lái)為了讀寫分離,二來(lái)order-query-service作為單獨(dú)應(yīng)用可以按需水平擴(kuò)容。
- 領(lǐng)域可以是多個(gè)子領(lǐng)域的一個(gè)虛擬集合,換句話說(shuō)多個(gè)微服務(wù)也可以形成一個(gè)大域,不必糾結(jié)于領(lǐng)域和微服務(wù)之間的數(shù)量對(duì)應(yīng)關(guān)系。我們?cè)谧黾軜?gòu)設(shè)計(jì)PPT的時(shí)候可能就把訂單域作為一個(gè)領(lǐng)域,代表了這個(gè)域就是關(guān)于訂單的,具體該有幾個(gè)微服務(wù),這需要更細(xì)的詳細(xì)設(shè)計(jì)來(lái)提供。
- 領(lǐng)域?qū)臃?wù)設(shè)計(jì)應(yīng)當(dāng)是調(diào)用者無(wú)關(guān)的。這一點(diǎn)有點(diǎn)像第一點(diǎn),但是它強(qiáng)調(diào)的是領(lǐng)域?qū)臃?wù)的設(shè)計(jì)不應(yīng)該受調(diào)用者的影響,這個(gè)觀點(diǎn)在《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì):軟件核心復(fù)雜性應(yīng)對(duì)之道》這本書里也可以找得到[4]。領(lǐng)域?qū)臃?wù)開發(fā)和設(shè)計(jì)的理念是關(guān)注自己的域,一旦邊界劃分清楚了,開發(fā)所需要考慮的永遠(yuǎn)都只是輸入和輸出,提供的服務(wù)一定是盡可能通用的,面向功能來(lái)開發(fā)的,而不是面向調(diào)用方來(lái)開發(fā)的。比如某個(gè)調(diào)用方提出了一個(gè)需求:調(diào)用方B希望A服務(wù)提供一個(gè)買汽車的接口,那么A服務(wù)設(shè)計(jì)的接口就應(yīng)該是buyCar(),而不是buyCarForB()。
5.Q&A
5.1 能不能在所有層使用數(shù)據(jù)持久層模型,簡(jiǎn)單快捷?
大家一定聽說(shuō)過(guò)不同層的數(shù)據(jù)模型的叫法不同的概念,比如數(shù)據(jù)持久層的模型對(duì)象叫DBO(database object)或者DPO(data persistence object),領(lǐng)域?qū)拥哪P蛯?duì)象叫DMO(Domain Model Object)或者就叫Model,數(shù)據(jù)傳輸層的模型對(duì)象叫DTO(Data Transfer Object)。那為啥要這么多模型呢,直接使用Mybatis等ORM框架生成DBO,然后一路吐給前端不是更爽(還真有同事嘗試立項(xiàng)寫Mybatis插件來(lái)實(shí)現(xiàn)這種所謂的代碼自動(dòng)化)。我個(gè)人建議如果您真的是要搭建一個(gè)復(fù)雜的大系統(tǒng),大平臺(tái),一定不要偷這種懶,最好的就是做到”一層一模型”(網(wǎng)關(guān)層使用應(yīng)用層模型即可)。各層之間采用手動(dòng)的數(shù)據(jù)賦值(getter,setter)來(lái)完成,或者使用一些轉(zhuǎn)換框架來(lái)簡(jiǎn)化轉(zhuǎn)換代碼,個(gè)人在用getter/setter時(shí)感覺并不會(huì)耽誤什么,在一個(gè)個(gè)set的時(shí)候,恰好可以對(duì)模型的字段細(xì)節(jié)進(jìn)一步確認(rèn),并且拒絕使用BeanUtils.copyProperties()這種工具類,因?yàn)檫@樣的工具類會(huì)讓”一層一模型”形同虛設(shè),開發(fā)會(huì)熱衷于把DPO拷貝到領(lǐng)域中換個(gè)名字以保證可以用拷貝工具。下面我們來(lái)細(xì)談下不能在每一層都是用數(shù)據(jù)持久層模型的具體原因:
- 應(yīng)用層對(duì)接網(wǎng)關(guān)層,是向面向C端或者調(diào)用者的一個(gè)數(shù)據(jù)出口,但是調(diào)用者只需要這個(gè)出口輸出用戶感興趣的數(shù)據(jù),并且有些敏感數(shù)據(jù)不能吐出去。如果應(yīng)用層(面向調(diào)用者)使用的仍然是數(shù)據(jù)庫(kù)模型,而開發(fā)人員沒(méi)有在應(yīng)用層把無(wú)關(guān)值置空的話(相信我,需求一多,工作一忙,鬼才會(huì)在意這些細(xì)節(jié)),那么數(shù)據(jù)庫(kù)里的整條記錄就作為接口輸出吐出去了。比如訂單記錄,用戶訂單列表可能只需要訂單ID,商品名稱,訂單金額。而像商家結(jié)算價(jià)這種就不能吐出去,萬(wàn)一被有心人察覺到了,用戶一定會(huì)投訴——你跟商家結(jié)算價(jià)200,賣給我400?
- 前端或者接口調(diào)用方會(huì)很痛苦,一個(gè)接口契約多一兩個(gè)無(wú)關(guān)字段是沒(méi)關(guān)系的,但是一個(gè)契約里給的30個(gè)字段,我只用到5個(gè),前端會(huì)罵娘的,我親眼見過(guò)這種事,設(shè)計(jì)原則里有個(gè)原則叫”迪米特法則”,也叫最小知識(shí)原則,接口設(shè)計(jì)也可以參考這個(gè)原則,盡量讓你的調(diào)用方知道盡可能少的信息點(diǎn)就能完成相關(guān)的任務(wù)。
- “一層一模型”本質(zhì)是解耦模型依賴。我在上家公司做架構(gòu)師時(shí)為了兼顧開發(fā)的感受,決定讓他們可以在領(lǐng)域?qū)雍突A(chǔ)設(shè)施層都是用數(shù)據(jù)持久層模型,而只需要在應(yīng)用層做數(shù)據(jù)控制(解決第一個(gè)問(wèn)題),然而我的妥協(xié)也慢慢露出弊端,開發(fā)有時(shí)候覺得某個(gè)數(shù)據(jù)庫(kù)字段命名不合適修改之后,整個(gè)引用了該模型的微服務(wù)都需要修改,如果一層一模型的話,只需要關(guān)聯(lián)數(shù)據(jù)庫(kù)訪問(wèn)的服務(wù)修改下DPO和DM的映射就行了,其他上層微服務(wù)都是依賴DM的。雖然我們不鼓勵(lì)隨意改動(dòng)數(shù)據(jù)庫(kù)字段,但設(shè)計(jì)框架上最好能支持這種情況。
剛開始推廣”一層一模型”的時(shí)候,會(huì)有耍小聰明的開發(fā)去把下一層的模型POJO直接拷貝過(guò)來(lái)改個(gè)名字,然后用BeanUtils.copyProperties()完成賦值,這樣跟直接使用數(shù)據(jù)持久層模型就沒(méi)有區(qū)別了,所以要杜絕這種情況的發(fā)生。
5.2 為啥需要應(yīng)用層,領(lǐng)域?qū)游⒎?wù)直接通過(guò)網(wǎng)關(guān)暴露不就行了嗎?
對(duì)于習(xí)慣了單體應(yīng)用開發(fā)者來(lái)說(shuō),一個(gè)微服務(wù)很可能就直觀對(duì)應(yīng)成了一個(gè)個(gè)垂直的應(yīng)用服務(wù),每個(gè)服務(wù)間的關(guān)系是這樣的:
其實(shí)這樣的體系本質(zhì)上仍然不能解決軟件的復(fù)雜性,這只是把系統(tǒng)簡(jiǎn)單粗暴的拆分了,耦合問(wèn)題仍然很嚴(yán)重,甚至這很有可能比原來(lái)的單體應(yīng)用更復(fù)雜(多對(duì)多依賴),如果使用微服務(wù)體系來(lái)處理復(fù)雜系統(tǒng),其服務(wù)體系應(yīng)當(dāng)是這樣的:
這兩幅圖的區(qū)別在于,其實(shí)第一幅圖中的每個(gè)服務(wù)都包含了完整的2~3層,所以不再需要單獨(dú)的應(yīng)用層。而第二幅圖各個(gè)領(lǐng)域模塊互相協(xié)作,對(duì)外提供服務(wù)時(shí),則需要有一層直面用戶需求的應(yīng)用層。Spring Boot 學(xué)習(xí)筆記,推薦學(xué)習(xí)下。
達(dá)成了微服務(wù)體系是解決復(fù)雜系統(tǒng)的出路之一這個(gè)共識(shí)后,我們?cè)賮?lái)看”應(yīng)用層服務(wù)存在的必要性”有哪些理由:
- 統(tǒng)一權(quán)限校驗(yàn):如上文所說(shuō),網(wǎng)關(guān)層只負(fù)責(zé)網(wǎng)絡(luò)級(jí)的安全防護(hù),業(yè)務(wù)層的權(quán)限校驗(yàn)則需要應(yīng)用層來(lái)完成,試想一個(gè)沒(méi)有應(yīng)用層的微服務(wù)體系,就意味著每一個(gè)微服務(wù)都需要加上權(quán)限校驗(yàn)邏輯,這不僅編碼上困難(可以用過(guò)濾器,AOP),而且對(duì)于成千上萬(wàn)個(gè)微服務(wù)(據(jù)了解,騰訊目前微服務(wù)數(shù)量已經(jīng)超過(guò)2萬(wàn),大眾點(diǎn)評(píng)有將近千個(gè)微服務(wù))來(lái)說(shuō),這會(huì)浪費(fèi)大量時(shí)間,調(diào)用鏈越長(zhǎng),浪費(fèi)的時(shí)間越多。換句話說(shuō),微服務(wù)體系有一個(gè)不突出但是很重要的特征—— 領(lǐng)域間環(huán)境安全,領(lǐng)域間的通信應(yīng)當(dāng)是可信的 ,否則分布式的缺點(diǎn)(多服務(wù)意味著多次通信)會(huì)被加劇。
- 業(yè)務(wù)數(shù)據(jù)網(wǎng)關(guān):舉個(gè)例子,一個(gè)order-service提供了一個(gè)queryOrder的接口,輸入起始日期查詢對(duì)應(yīng)的訂單列表,其有2個(gè)消費(fèi)者:C端網(wǎng)站應(yīng)用服務(wù) 和 報(bào)表應(yīng)用服務(wù) ,C端網(wǎng)站應(yīng)用服務(wù) 只需要知道訂單的基本信息如下單時(shí)間、商品名稱、金額就可以了,而報(bào)表應(yīng)用服務(wù)是給管理者看的,需要的訂單數(shù)據(jù)很全,除了C端網(wǎng)站應(yīng)用服務(wù)需要的之外,還需要看平臺(tái)與商家的結(jié)算金額。根據(jù)第4部分最后一點(diǎn)的思路,我們肯定不能為調(diào)用方寫定制接口(寫不完的,有的要這個(gè)數(shù)據(jù),有的要那個(gè)數(shù)據(jù),每次新增調(diào)用方,領(lǐng)域服務(wù)還得找人修改)。而如果我們統(tǒng)一使用的全量數(shù)據(jù),并且沒(méi)有應(yīng)用層(同樣的也沒(méi)有應(yīng)用層模型DTO了),那么很可能我們吐出去的數(shù)據(jù)包含了我們與商家的結(jié)算價(jià),這會(huì)引發(fā)很多不必要的麻煩的。所以應(yīng)用層還充當(dāng)了業(yè)務(wù)數(shù)據(jù)網(wǎng)關(guān)的作用,應(yīng)用層應(yīng)用服務(wù)需要保證僅吐出調(diào)用方感興趣的數(shù)據(jù)。
- 資源控制和緩存:想象一下雙十一高并發(fā)的情況,如果查詢庫(kù)存每次都查庫(kù)是多么恐怖的一件事。所以一般僅在支付的時(shí)候做一次庫(kù)存校驗(yàn),而在商品展示時(shí)查緩存的庫(kù)存即可。那么問(wèn)題來(lái)了,如果沒(méi)有應(yīng)用層,緩存直接放在庫(kù)存微服務(wù)上是否可行呢?首先這會(huì)入侵庫(kù)存領(lǐng)域,庫(kù)存微服務(wù)需要按照調(diào)用方的需求做特定時(shí)間的緩存,而不是自己想緩存多久就多久,我想庫(kù)存微服務(wù)的開發(fā)者也會(huì)很不滿的,他會(huì)提出,讓你自己去做緩存。他的方案是科學(xué)的,因?yàn)檫€有一些其他服務(wù)可能需要實(shí)時(shí)的數(shù)據(jù)。這時(shí)候就需要有一層來(lái)做對(duì)其下方微服務(wù)返回的數(shù)據(jù)按照應(yīng)用自身的需求進(jìn)行必要的緩存,而不是把這些需求都推給資源提供方,想象一下一個(gè)資源提供方有多少需求者,每個(gè)需求方都有自己的定制需求,該多痛苦。當(dāng)然這一點(diǎn)也不是說(shuō)微服務(wù)自身不能做緩存,微服務(wù)自身的緩存一定是考慮自身域的合理性后的一個(gè)措施(比如訂單查詢服務(wù)會(huì)做一個(gè)500ms的緩存,因?yàn)椴粫?huì)有正常人500ms里點(diǎn)兩次查詢還必須要求兩次都是最新的),而不是由調(diào)用方來(lái)決定的。
- 資源聚合和加工:其實(shí)第2點(diǎn)也有加工的味道在里面,只是這里更多的是描述應(yīng)用層應(yīng)用根據(jù)自身需求來(lái)對(duì)下層返回的數(shù)據(jù)進(jìn)行聚合和處理的過(guò)程。舉個(gè)例子就能很好的說(shuō)明這一點(diǎn):任何APP都有首頁(yè),而首頁(yè)的數(shù)據(jù)可能是五花八門的,可以有用戶昵稱、最近下的訂單簡(jiǎn)要信息、最近支出曲線、積分信息等。這4個(gè)信息可以來(lái)自4個(gè)領(lǐng)域微服務(wù),他們是:用戶中心、訂單中心、支付中心和積分中心。那么有讀者會(huì)說(shuō),直接暴露微服務(wù)讓前端分別調(diào)用4個(gè)接口再做聚合不是也行嗎?顯然這種粗暴的方式是極其不合理的,會(huì)額外增加廣域網(wǎng)網(wǎng)絡(luò)調(diào)用3次不說(shuō),還傳輸了很多不必要的信息。
- 應(yīng)用隔離和流控:如果將每個(gè)領(lǐng)域服務(wù)直接暴露到網(wǎng)關(guān)層對(duì)外提供服務(wù),那么在多應(yīng)用場(chǎng)景下,多個(gè)應(yīng)用間是共享這些服務(wù)能力的,在服務(wù)降級(jí)的時(shí)候,如果需要按照應(yīng)用進(jìn)行降級(jí)(比如將優(yōu)先級(jí)不高的應(yīng)用進(jìn)行限流),就很難實(shí)現(xiàn)。但如果每個(gè)應(yīng)用對(duì)應(yīng)了一個(gè)應(yīng)用層服務(wù),只需要對(duì)其暴露的網(wǎng)關(guān)接口進(jìn)行統(tǒng)一限流就行了,或者在應(yīng)用層做一個(gè)開關(guān),將其流量阻止在應(yīng)用層,而不是拖垮整個(gè)領(lǐng)域服務(wù)。舉個(gè)例子,假如我們的平臺(tái)不僅有自己的網(wǎng)站服務(wù),還有第三方的對(duì)接服務(wù),如果某個(gè)第三方被攻擊而我們直接將領(lǐng)域服務(wù)暴露了出去,那么我們就需要在各個(gè)領(lǐng)域?qū)臃?wù)里去編寫對(duì)應(yīng)的開關(guān),這將侵入領(lǐng)域?qū)臃?wù),導(dǎo)致不必要的耦合。而有了應(yīng)用層這些都不是問(wèn)題,因?yàn)閼?yīng)用層充當(dāng)了一個(gè)調(diào)度者的角色,調(diào)度者可以很輕松的決定是否調(diào)度下層的服務(wù)。
為了加深對(duì)應(yīng)用層的理解,我們舉個(gè)代碼的例子,假如我們寫一個(gè)很簡(jiǎn)單的首頁(yè)應(yīng)用:
- Response getHomeData(Request request){
- String nickName = userService.getNickName(request.getUserId());
- OrderInfo orderInfo = orderService.queryLatestOrder(request.getUserId());
- CostTrend costTrend = payService.queryCostTrend(request.getUserId());
- Integer points = pointService.queryAvailablePoints(request.getUserId());
- return new Response(nickName, orderInfo, costTrend, points);
- }
這里的4個(gè)服務(wù)類實(shí)例userService,orderService、payService和pointService如果都是本地的方法,那么這就是一個(gè)單體應(yīng)用,而微服務(wù)化后這4個(gè)可能都是微服務(wù)了,但是應(yīng)用層應(yīng)用的結(jié)構(gòu)還是可以不用變化(現(xiàn)在很多的RPC框架都做到了與調(diào)用本地方法無(wú)差別)。這就是應(yīng)用層的位置所在。另外,微服務(wù)系列面試題和答案全部整理好了,微信搜索Java技術(shù)棧,在后臺(tái)發(fā)送:面試,可以在線閱讀。
5.3 什么是反模式?
這里的反模式是指《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì):軟件核心復(fù)雜性應(yīng)對(duì)之道》這本書里提到的與DDD相違背的模式,也是Eric極其反對(duì)的一種模式,即SmartUI模式(注意反模式不等于SmartUI,只是在本書中作為一個(gè)反模式的例子而已),這是一種什么樣的模式呢,其實(shí)我很早之前做C++ Builder(和Delphi很像)的時(shí)候還不知道,C++ Builder就是一種SmartUI模式。但其實(shí)SmartUI并沒(méi)有錯(cuò),對(duì)于小規(guī)模的PC本地應(yīng)用開發(fā)來(lái)說(shuō)也是有很多好處的。舉個(gè)例子,C++ Builder中在窗體上添加一個(gè)按鈕,然后雙擊按鈕添加事件,這樣就跟實(shí)際操作的時(shí)候有機(jī)的結(jié)合了起來(lái)。
換句話說(shuō)就是使用界面驅(qū)動(dòng)業(yè)務(wù)開發(fā)。在大型系統(tǒng)的開發(fā)上,這種模式是害人精,我很理解Eric為啥這么討厭它。曾有一次我?guī)ьI(lǐng)著一個(gè)團(tuán)隊(duì)做封閉式開發(fā),在過(guò)完產(chǎn)品需求后,家里出了點(diǎn)事我請(qǐng)了幾天假,回來(lái)后發(fā)現(xiàn)產(chǎn)品經(jīng)理竟然指揮讓開發(fā)按照UI原型來(lái)設(shè)計(jì)數(shù)據(jù)庫(kù),我審核的時(shí)候發(fā)現(xiàn)這些開發(fā)設(shè)計(jì)的表有極其多的冗余,而有一些重要的過(guò)程變量值卻沒(méi)有考慮到。比如他們會(huì)為每個(gè)頁(yè)面建幾個(gè)表,這顯然是行不通的,科學(xué)的方法是拆分領(lǐng)域,每個(gè)領(lǐng)域自己建立自己的表。UI只是應(yīng)用層整合了各領(lǐng)域服務(wù)的數(shù)據(jù)并且處理后輸出的一種展示。
5.4 分層設(shè)計(jì)的開發(fā)步驟是怎樣的?
假設(shè)我們以一個(gè)標(biāo)準(zhǔn)的SaaS項(xiàng)目為主,也就是表示層是前端頁(yè)面(可以是APP,H5,M站,小程序,PC站等),那么高效的一種開發(fā)步驟可以是這樣的:
- 業(yè)務(wù)、產(chǎn)品、開發(fā)PM進(jìn)行需求評(píng)審(可行性等)
- 產(chǎn)品準(zhǔn)備好原型
- 產(chǎn)品、開發(fā)(前后端)、架構(gòu)師(或有架構(gòu)師能力的資深開發(fā))開會(huì)過(guò)PRD,了解要做什么
- 架構(gòu)師開始設(shè)計(jì)領(lǐng)域(資深架構(gòu)師一下午就能搞定),前端開始切圖,應(yīng)用層開發(fā)開始按照UI和PRD設(shè)計(jì)前端每個(gè)頁(yè)面使用的Restful接口(比如直接Springfox代碼生成Swagger)
- 架構(gòu)師設(shè)計(jì)完領(lǐng)域后分工給領(lǐng)域?qū)娱_發(fā),進(jìn)行領(lǐng)域邊界明確,然后領(lǐng)域?qū)娱_發(fā)開始設(shè)計(jì)數(shù)據(jù)庫(kù)表等。
- 這樣前后端開發(fā)就同時(shí)開工了。
- 開發(fā)初步完成后,自測(cè)加連調(diào)。
- 后續(xù)就是測(cè)試發(fā)布了。
具體階段和時(shí)間線可以參考下圖:
6.結(jié)語(yǔ)
真實(shí)技術(shù)開發(fā)日常討論的經(jīng)常是高并發(fā)、多線程、大數(shù)據(jù),分布式、RPC,很少有人討論軟件架構(gòu)設(shè)計(jì)方面的,架構(gòu)師文化不應(yīng)該只是對(duì)某個(gè)技術(shù)點(diǎn)的深入挖掘,也應(yīng)該多討論些大型軟件設(shè)計(jì)理念方面的。