如何構(gòu)建基于 DDD 領(lǐng)域驅(qū)動的微服務(wù)?
點擊關(guān)注公眾號,Java干貨及時送達
盡管微服務(wù)中的“微”一詞表示服務(wù)的規(guī)模,但它并不是使用微服務(wù)的唯一標(biāo)準(zhǔn)。當(dāng)團隊轉(zhuǎn)向基于微服務(wù)的架構(gòu)時,他們旨在提高敏捷性以及自主且頻繁地部署功能。很難確定這種架構(gòu)風(fēng)格的簡單定義。我喜歡Adrian Cockcroft的關(guān)于微服務(wù)的簡短定義:“ 面向服務(wù)的體系結(jié)構(gòu),它由松散耦合的、具有上下文邊界的元素組成。”
服務(wù)具有圍繞業(yè)務(wù)上下文而不是任意技術(shù)上抽象的明確定義的邊界 通過意圖公開界面隱藏實現(xiàn)細節(jié)并公開功能 服務(wù)不會共享超出其邊界的內(nèi)部結(jié)構(gòu)。例如,不共享數(shù)據(jù)庫。 服務(wù)可以抵抗故障。 團隊獨立擁有職能,并能夠自主發(fā)布變更 團隊擁護自動化文化。例如,自動化測試,持續(xù)集成和持續(xù)交付 簡而言之,我們可以將這種體系結(jié)構(gòu)樣式總結(jié)如下:
松耦合的面向服務(wù)的體系結(jié)構(gòu),其中每個服務(wù)都包含在定義明確的有界上下文中,從而可以快速,頻繁且可靠地交付應(yīng)用程序。
微服務(wù)設(shè)計從這些概念中汲取了靈感,因為所有這些原理都有助于構(gòu)建可以相互獨立變化和發(fā)展的模塊化系統(tǒng)。
領(lǐng)域:代表組織的工作。例如它是零售或電子商務(wù)。
子域:組織或組織內(nèi)的業(yè)務(wù)部門。域由多個子域組成。
微服務(wù)與有限上下文如何相關(guān)
現(xiàn)在,微服務(wù)在哪里適合?可以說每個有界上下文都映射到微服務(wù)嗎?是的,我們將明白為什么。在某些情況下,有界上下文的邊界或輪廓可能很大。
考慮上面的例子。定價綁定上下文具有三個不同的模型-價格,定價項目和折扣,每個模型負責(zé)目錄項目的價格,計算項目列表的總價格并分別應(yīng)用折扣。我們可以創(chuàng)建一個包含以上所有模型的系統(tǒng),但是它可能會成為一個不合理的大型應(yīng)用程序。如前所述,每個數(shù)據(jù)模型都有其不變性和業(yè)務(wù)規(guī)則。隨著時間的流逝,如果我們不小心的話,系統(tǒng)可能會變成一個泥濘的大球,邊界模糊,職責(zé)重疊,甚至可能回到我們開始的地方—一個整體。
對系統(tǒng)進行建模的另一種方法是將相關(guān)模型分離或分組為單獨的微服務(wù)。在DDD中,這些模型(價格,定價項目和折扣)被稱為聚合Aggregates。聚合是組成相關(guān)模型的獨立模型。您只能通過已發(fā)布的界面更改聚合的狀態(tài),并且聚合可確保一致性,并且不變量保持良好狀態(tài)。
聚合是關(guān)聯(lián)對象的集群,被視為數(shù)據(jù)更改的單元。外部引用僅限于AGGREGATE的一個成員,稱為根。一組一致性規(guī)則適用于AGGREGATE的邊界。推薦一個 Spring Boot 基礎(chǔ)教程及實戰(zhàn)示例:https://www.javastack.cn/categories/Spring-Boot/
上下文映射的完整探索不在本博客的討論范圍之內(nèi),但我們將通過一個示例進行說明。下圖顯示了處理電子商務(wù)訂單付款的各種應(yīng)用程序。
購物車上下文負責(zé)訂單的在線授權(quán);訂單上下文流程過帳付款后的付款流程;聯(lián)絡(luò)中心會處理所有例外情況,例如重試付款和更改用于訂單的付款方式 為了簡單起見,讓我們假設(shè)所有這些上下文都是作為單獨的服務(wù)實現(xiàn)的 所有這些上下文封裝了相同的模型。 請注意,這些模型在邏輯上是相同的。也就是說,它們都遵循相同的通用域語言-付款方式,授權(quán)和結(jié)算。只是它們是不同上下文的一部分。 重新定義服務(wù)邊界—將聚合映射到正確的上下文
錯誤案例如下圖:
電子商務(wù)中所有模型都直接與單個支付聚合的網(wǎng)關(guān)上下文(payment gateway context)集成,支付需要保證事務(wù)性,但是由于與多個服務(wù)集成,支付的事務(wù)性就不能通過在各種服務(wù)之間強制執(zhí)行不變性和一致性來實現(xiàn),(banq注:當(dāng)然有人就提出分布式事務(wù)的概念來在這些不同服務(wù)之間實現(xiàn)支付過程的事務(wù)性,這其實是在錯誤設(shè)計基礎(chǔ)上的偽概念)。
另外,分布式事務(wù)系列面試題和答案全部整理好了,微信搜索Java技術(shù)棧,在后臺發(fā)送:面試,可以在線閱讀。
請注意,支付網(wǎng)關(guān)中的任何更改都將迫使更改多個服務(wù),并可能更改多個團隊,因為不同的組可以擁有這些上下文。
進行一些調(diào)整并使聚合與正確的上下文對齊,我們可以更好地表示這些子域如下圖。發(fā)生了很多變化。讓我們回顧一下更改:
通常,整體(單體)或遺留應(yīng)用程序具有許多聚合,通常具有重疊的邊界。創(chuàng)建這些聚合及其依賴關(guān)系的上下文地圖有助于我們了解將要從這些整體中擺脫出來的任何新微服務(wù)的輪廓。請記住,微服務(wù)架構(gòu)的成功或失敗取決于聚合之間的低耦合以及這些聚合之間的高內(nèi)聚性。
同樣重要的是要注意,有界上下文本身就是合適的內(nèi)聚單元。即使上下文具有多個聚合,整個上下文及其聚合也可以組成一個微服務(wù)。我們發(fā)現(xiàn)這種啟發(fā)式方法對于有些晦澀的領(lǐng)域特別有用-考慮組織正在涉足的新業(yè)務(wù)領(lǐng)域。您可能對分離的正確邊界沒有足夠的了解,并且聚集體的任何過早分解都可能導(dǎo)致昂貴的重構(gòu)。想象一下,由于數(shù)據(jù)遷移,不得不將兩個數(shù)據(jù)庫合并為一個,因為我們偶然發(fā)現(xiàn)兩個聚合屬于同一類。但是請確保通過接口將這些聚合充分隔離,以使它們不知道彼此的復(fù)雜細節(jié)。
點擊關(guān)注公眾號,Java干貨及時送達 事件風(fēng)暴-識別服務(wù)邊界的另一種技術(shù)
微服務(wù)之間的通信
一個整體在一個流程邊界內(nèi)托管了多個聚合體。因此,在此邊界內(nèi)可以管理聚合體的事務(wù)一致性,例如,如果客戶下訂單,我們可以減少項目的庫存,并向客戶發(fā)送電子郵件,全部都在一次交易事務(wù)中。所有操作都會成功,或者全部都會失敗。
但是,當(dāng)我們打破整體并將聚合散布到不同的環(huán)境中時,我們將擁有數(shù)十甚至數(shù)百個微服務(wù)。迄今為止,在整體結(jié)構(gòu)的單個邊界內(nèi)存在的流程現(xiàn)在分布在多個分布式系統(tǒng)中。要在所有這些分布式系統(tǒng)上實現(xiàn)事務(wù)的完整性和一致性非常困難,而且要付出代價-系統(tǒng)的可用性。
另外,關(guān)注公眾號Java技術(shù)棧,在后臺回復(fù):面試,可以獲取我整理的 Java 分布式系列面試題和答案,非常齊全。
微服務(wù)也是分布式系統(tǒng)。因此,CAP定理也適用于它們- “分布式系統(tǒng)只能提供三個所需特征中的兩個:一致性,可用性和分區(qū)容限(CAP中的“ C”,“ A”和“ P”)。” 在現(xiàn)實世界的系統(tǒng)中,分區(qū)容忍度是無法商議的- 網(wǎng)絡(luò)不可靠,虛擬機可能宕機,區(qū)域之間的延遲會變得更糟,等等。
因此,我們可以選擇“可用性”或“一致性”。現(xiàn)在,我們知道在任何現(xiàn)代應(yīng)用程序中,犧牲可用性也不是一個好主意。
圍繞最終一致性設(shè)計應(yīng)用程序
如果您嘗試跨多個分布式系統(tǒng)構(gòu)建事務(wù),那么您將再次陷入困境。變成最糟糕的一種分布式整體事務(wù)。如果任何一個系統(tǒng)點不可用,則整個流程將不可用,通常會導(dǎo)致令人沮喪的客戶體驗、失去保障失敗的承諾等。
此外,對一項服務(wù)的更改通常可能需要對另一項服務(wù)進行更改,從而導(dǎo)致復(fù)雜而昂貴的部署。因此,我們最好根據(jù)自己的用例來設(shè)計應(yīng)用程序,以容忍一點點的不一致性,以提高可用性。對于上面的示例,我們可以使所有進程異步,從而最終保持一致。我們可以異步發(fā)送電子郵件,而與其他流程無關(guān)。
如果承諾的物品以后在倉庫中不可用,該項目可能被延期訂購,或者我們可以停止接受超過某個閾值的項目的訂單。
有時,您可能會遇到一個場景,該場景可能需要跨越不同流程邊界的兩個聚合中的強ACID樣式事務(wù)。這是重新查看這些聚合并將它們合并為一個極好的標(biāo)志。
在我們開始在不同過程邊界中分解這些聚合之前,事件風(fēng)暴和上下文映射將有助于及早識別這些依賴性。將兩個微服務(wù)合并為一個成本很高,這是我們應(yīng)該努力避免的事情。
最新面試題整理好了,點擊Java面試庫小程序在線刷題。
支持事件驅(qū)動的架構(gòu)
微服務(wù)可能會在其聚合上發(fā)出本質(zhì)上的變化。這些稱為領(lǐng)域事件,并且對這些更改感興趣的任何服務(wù)都可以偵聽這些事件并在其域內(nèi)采取相應(yīng)的操作。這種方法避免了任何行為上的耦合:一個域不規(guī)定其他域應(yīng)該做什么,以及時間耦合-一個過程的成功完成不依賴于同時可用的所有系統(tǒng)。當(dāng)然,這將意味著系統(tǒng)最終將保持一致。
在上面的示例中,訂單服務(wù)發(fā)布了一個事件-訂單已取消。訂閱該事件的其他服務(wù)處理其各自的域功能:付款服務(wù)退還款項,庫存服務(wù)調(diào)整項目的庫存,依此類推。要確保此集成的可靠性和彈性的幾點注意事項:
生產(chǎn)者應(yīng)確保至少生產(chǎn)一次事件。如果這樣做失敗,則應(yīng)確保存在回退機制以重新觸發(fā)事件 消費者應(yīng)確保以冪等的方式消費事件。如果再次發(fā)生同一事件,那么在用戶端不應(yīng)有任何副作用。事件也可能不按順序到達。消費者可以使用時間戳記或版本號字段來保證事件的唯一性。 由于某些用例的性質(zhì),不一定總是可以使用基于事件的集成。請查看購物車服務(wù)和付款服務(wù)之間的集成。這是一個同步集成,因此我們需要注意一些事項。這是行為耦合的一個示例-Cart服務(wù)可能從Payment服務(wù)中調(diào)用REST API,并指示其授權(quán)訂單付款,而時間耦合則需要Payment服務(wù)用于Cart服務(wù)才能接受訂單。這種耦合減少了這些上下文的自治性,并且可能減少了不希望的依賴性。有幾種方法可以避免這種耦合,但是使用所有這些選項,我們將失去向客戶提供即時反饋的能力。
將REST API轉(zhuǎn)換為基于事件的集成。但是,如果支付服務(wù)僅公開REST API,則此選項可能不可用 購物車服務(wù)立即接受訂單,并且有一個批處理作業(yè)來接管訂單并調(diào)用支付服務(wù)API 購物車服務(wù)會產(chǎn)生一個本地事件,然后調(diào)用付款服務(wù)API 在失敗和上游依賴項(付款服務(wù))不可用的情況下,將上述內(nèi)容與重試結(jié)合使用可以使設(shè)計更具彈性。例如,在發(fā)生故障的情況下,可以通過事件或基于批次的重試來備份購物車和付款服務(wù)之間的同步集成。
推薦一個 Spring Boot 基礎(chǔ)教程及實戰(zhàn)示例:
https://www.javastack.cn/categories/Spring-Boot/
這種方法會對客戶體驗產(chǎn)生額外的影響:客戶可能輸入了不正確的付款明細,并且當(dāng)我們離線處理付款時,我們不會將其在線。否則,收回失敗的付款可能會增加業(yè)務(wù)成本。但是,很有可能,購物車服務(wù)對于支付服務(wù)的不可用性或故障具有彈性,其缺點勝于缺點。例如,如果我們無法離線收款,我們可以通知客戶。
避免針對特定消費者數(shù)據(jù)需求的服務(wù)之間的編排
任何面向服務(wù)的體系結(jié)構(gòu)中的反模式之一是,這些服務(wù)都可以滿足消費者的特定訪問模式。通常,這發(fā)生在消費者團隊與服務(wù)團隊緊密合作時。如果團隊正在開發(fā)整體應(yīng)用程序,則他們通常會創(chuàng)建一個跨不同聚合邊界的單一API,從而緊密耦合這些聚合。
讓我們考慮一個例子。說Web中的“訂單詳細信息”頁面,移動應(yīng)用程序需要在單個頁面上同時顯示訂單的詳細信息和針對該訂單處理的退款的詳細信息。在整體應(yīng)用程序中,Order GET API(假設(shè)它是REST API)一起查詢Orders和Refunds,合并兩個聚合,然后將復(fù)合響應(yīng)發(fā)送給調(diào)用方。由于聚合屬于相同的過程邊界,因此無需太多開銷即可執(zhí)行此操作。因此,消費者可以在一個調(diào)用中獲得所有必要的數(shù)據(jù)。
如果訂單和退款是不同上下文的一部分,則數(shù)據(jù)不再存在于單個微服務(wù)或聚合邊界內(nèi)。為消費者保留相同功能的一種選擇是使訂單服務(wù)負責(zé)調(diào)用退款服務(wù)并創(chuàng)建復(fù)合響應(yīng)。此方法引起以下問題:
訂單服務(wù)現(xiàn)在與另一個服務(wù)集成在一起,純粹是為了支持需要退款數(shù)據(jù)和訂單數(shù)據(jù)的消費者。現(xiàn)在,訂單服務(wù)的自治性降低了,因為退款總額中的任何更改都將導(dǎo)致訂單總額中的更改。 訂單服務(wù)具有另一個集成,因此要考慮另一個故障點-如果退款服務(wù)出現(xiàn)故障,訂購服務(wù)是否仍可以發(fā)送部分數(shù)據(jù),并且消費者可以正常地故障嗎? 如果消費者需要更改以從“退款”聚合中獲取更多數(shù)據(jù),則現(xiàn)在需要兩個團隊來進行更改 如果在整個平臺上都遵循這種模式,則可能導(dǎo)致各種域服務(wù)之間的依存關(guān)系錯綜復(fù)雜,所有這些都是因為這些服務(wù)滿足了調(diào)用者的特定訪問模式。 前端的后端BFF
減輕這種風(fēng)險的一種方法是讓消費者團隊管理各種域服務(wù)之間的編排。畢竟,呼叫者會更好地了解訪問模式,并且可以完全控制對這些模式的任何更改。這種方法將域服務(wù)與表示層分離開來,使它們專注于核心業(yè)務(wù)流程。但是,如果Web和移動應(yīng)用程序開始直接調(diào)用不同的服務(wù)而不是從整體中調(diào)用一個復(fù)合API,則可能會導(dǎo)致這些應(yīng)用程序的性能開銷–通過較低帶寬網(wǎng)絡(luò)進行多次調(diào)用,處理和合并來自不同API的數(shù)據(jù),等等。。
相反,可以使用另一種稱為前端的后端的模式。在這種設(shè)計模式下,由消費者(在本例中為Web和移動團隊)創(chuàng)建和管理的后端服務(wù)負責(zé)跨多個域服務(wù)的集成,純粹是為了向客戶提供前端體驗。Web和移動團隊現(xiàn)在可以根據(jù)他們的用例設(shè)計數(shù)據(jù)合同。他們甚至可以使用GraphQL而不是REST API來靈活地查詢并準(zhǔn)確獲取所需的信息。
重要的是要注意,此服務(wù)是由使用者團隊擁有和維護的,而不是由擁有域服務(wù)的團隊擁有和維護的。前端團隊現(xiàn)在可以根據(jù)自己的需求進行優(yōu)化-移動應(yīng)用程序可以請求更小的有效負載,減少來自移動應(yīng)用程序的呼叫次數(shù)等等。查看下面的業(yè)務(wù)流程的修訂視圖。
結(jié)論
在此博客中,我們觸及了各種概念,策略和設(shè)計啟發(fā)法,以便在我們進入微服務(wù)領(lǐng)域時,尤其是在嘗試將整體式服務(wù)拆分為基于域的微服務(wù)時,加以考慮。
其中許多主題本身就是廣闊的主題,我認為我們沒有做出足夠的公正性來詳細解釋它們,但是我們想介紹一些關(guān)鍵主題以及我們采用這些主題的經(jīng)驗。進一步閱讀(鏈接)部分提供了一些參考資料和一些有用的內(nèi)容,供任何希望采用此方法的人使用。
原文:https://medium.com/walmartglobaltech/building-domain-driven-microservices-af688aa1b1b8
譯文:jdon.com/54558
微信官宣:一大波新年紅包封面來了! 2021 年發(fā)生的 10 件技術(shù)大事!! 23 種設(shè)計模式實戰(zhàn)(很全) Log4j2 漏洞之 JNDI 到底是個什么鬼? 炸了!Log4j2 再爆漏洞。。 勁爆!Java 協(xié)程要來了! 重磅官宣:Redis 對象映射框架來了!! 推薦一款代碼神器,代碼量至少省一半! 程序員精通各種技術(shù)體系,45歲求職難! 重磅!Spring Boot 2.6 正式發(fā)布 Spring Boot 學(xué)習(xí)筆記,這個太全了!
關(guān)注Java技術(shù)棧看更多干貨
獲取 Spring Boot 實戰(zhàn)筆記!







