如何構建基於 DDD 領域驅動的微服務?

儘管微服務中的“微”一詞表示服務的規模,但它並不是使用微服務的唯一標準。當團隊轉向基於微服務的架構時,他們旨在提高敏捷性以及自主且頻繁地部署功能。很難確定這種架構風格的簡單定義。我喜歡Adrian Cockcroft的關於微服務的簡短定義: “ 面向服務的體系結構,它由鬆散耦合的、具有上下文邊界的元素組成。”

儘管這定義了高級設計啓發式技術,但微服務架構具有一些獨特的特性,使其有別於以往的面向服務的架構。以下是其中一些特徵。這些以及其他一些文檔都有據可查-Martin Fowler的文章和Sam Newman的Building Microservices,僅舉幾例。

  1. 服務具有圍繞業務上下文而不是任意技術上抽象的明確定義的邊界
  2. 通過意圖公開界面隱藏實現細節並公開功能
  3. 服務不會共享超出其邊界的內部結構。例如,不共享數據庫。
  4. 服務可以抵抗故障。
  5. 團隊獨立擁有職能,並能夠自主發佈變更
  6. 團隊擁護自動化文化。例如,自動化測試,持續集成和持續交付

簡而言之,我們可以將這種體系結構樣式總結如下:

松耦合的面向服務的體系結構,其中每個服務都包含在定義明確的有界上下文中,從而可以快速,頻繁且可靠地交付應用程序。

領域驅動設計和有界上下文

微服務的力量來自明確定義其職責並劃分它們之間的邊界。此處的目的是在邊界內建立高凝聚力,並在邊界外建立低耦合(banq注:高凝聚低耦合)。也就是說,趨於一起改變的事物應該屬於同一事物。就像在許多現實生活中的問題一樣,這說起來容易做起來難,業務在不斷髮展,邏輯的假設前提也會隨之改變。因此,重構的能力是設計系統時要考慮的另一關鍵問題。

領域驅動設計(DDD)是關鍵,在我們看來,這是設計微服務時必不可少的工具,無論是打破整體還是實施未開發項目。領域驅動的設計(Eric Evans在他的書中提出)是一組思想、原理和模式,可幫助基於業務領域的基礎模型設計軟件系統。開發人員和領域專家共同合作,以通用的通用語言創建業務模型。然後,他們將這些模型綁定到有意義的系統,並在這些系統與從事這些服務的團隊之間建立協作協議。更重要的是,他們設計了系統之間的概念輪廓或邊界。

微服務設計從這些概念中汲取了靈感,因爲所有這些原理都有助於構建可以相互獨立變化和發展的模塊化系統。

在繼續進行之前,讓我們快速瞭解一下DDD的一些基本術語。域驅動設計的完整概述超出了本博客的範圍。我們強烈建議任何嘗試構建微服務的人推薦Eric Evans的書籍。

領域:代表組織的工作。例如它是零售或電子商務。

子域:組織或組織內的業務部門。域由多個子域組成。

無所不在的語言:這是用於表達模型的語言。在下面的示例中,Item是一個模型,屬於這些子域中每個子域的通用語言。開發人員,產品經理,領域專家和業務涉衆都同意使用相同的語言,並在其工件(代碼,產品文檔等)中使用該語言。

有界上下文:域驅動的設計將有界上下文定義爲“單詞或語句能確定其含義的設置”。簡而言之,這意味着模型是有意義的邊界。在上面的示例中,“項目”在每種上下文中的含義不同。在目錄上下文中,項目表示可售產品,而在購物車上下文中,則表示客戶已將其添加到購物車中的項目。在“運輸”上下文中,它表示將要運送給客戶的倉庫物料。這些模型中的每一個都是不同的,並且每個都有不同的含義,並且可能包含不同的屬性。通過將這些模型分離並隔離在它們各自的邊界內,我們可以自由地表達模型而沒有歧義。

注意:必須瞭解子域和有界上下文之間的區別。子域屬於問題空間,即您的企業如何看待問題,而受限上下文屬於解決方案空間,即我們將如何實施問題的解決方案。從理論上講,每個子域可能具有多個有界上下文,儘管我們努力爲每個子域提供一個有界上下文。

微服務與有限上下文如何相關

現在,微服務在哪裏適合?可以說每個有界上下文都映射到微服務嗎?是的,我們將明白爲什麼。在某些情況下,有界上下文的邊界或輪廓可能很大。

考慮上面的例子。定價綁定上下文具有三個不同的模型-價格,定價項目和折扣,每個模型負責目錄項目的價格,計算項目列表的總價格並分別應用折扣。我們可以創建一個包含以上所有模型的系統,但是它可能會成爲一個不合理的大型應用程序。如前所述,每個數據模型都有其不變性和業務規則。隨着時間的流逝,如果我們不小心的話,系統可能會變成一個泥濘的大球,邊界模糊,職責重疊,甚至可能回到我們開始的地方—一個整體。

對系統進行建模的另一種方法是將相關模型分離或分組爲單獨的微服務。在DDD中,這些模型(價格,定價項目和折扣)被稱爲聚合Aggregates。聚合是組成相關模型的獨立模型。您只能通過已發佈的界面更改聚合的狀態,並且聚合可確保一致性,並且不變量保持良好狀態。

聚合是關聯對象的集羣,被視爲數據更改的單元。外部引用僅限於AGGREGATE的一個成員,稱爲根。一組一致性規則適用於AGGREGATE的邊界。

同樣,沒有必要一定要將每個聚合建模爲一個獨特的微服務。圖中的服務(聚合)是一對一關係,但這不一定是規則。在某些情況下,在單個服務中託管多個聚合可能是有意義的,尤其是當我們不完全瞭解業務領域時。需要注意的重要一點是,只能在單個聚合中保證一致性,並且只能通過已發佈的界面修改聚合。任何違反這些規定的行爲都有變成大泥球的風險。

上下文映射—精確劃分微服務邊界的一種方法

整體結構通常由不同的模型組成,大多數模型是緊密耦合的-模型可能知道彼此的親密細節,更改一個模型可能會對另一個模型產生副作用,依此類推。分解整體時,確定這些模型(在這種情況下爲集合)及其關係至關重要。上下文映射可以幫助我們做到這一點。它們用於標識和定義各種有界上下文和聚合之間的關係。在上面的示例中,有界上下文定義了模型的邊界(價格,折扣等),而上下文映射定義了這些模型之間以及不同有界上下文之間的關係。

上下文映射的完整探索不在本博客的討論範圍之內,但我們將通過一個示例進行說明。下圖顯示了處理電子商務訂單付款的各種應用程序。

  1. 購物車上下文負責訂單的在線授權;訂單上下文流程過帳付款後的付款流程;聯絡中心會處理所有例外情況,例如重試付款和更改用於訂單的付款方式
  2. 爲了簡單起見,讓我們假設所有這些上下文都是作爲單獨的服務實現的
  3. 所有這些上下文封裝了相同的模型。
  4. 請注意,這些模型在邏輯上是相同的。也就是說,它們都遵循相同的通用域語言-付款方式,授權和結算。只是它們是不同上下文的一部分。

重新定義服務邊界—將聚合映射到正確的上下文

錯誤案例如下圖:

電子商務中所有模型都直接與單個支付聚合的網關上下文(payment gateway context)集成,支付需要保證事務性,但是由於與多個服務集成,支付的事務性就不能通過在各種服務之間強制執行不變性和一致性來實現,(banq注:當然有人就提出分佈式事務的概念來在這些不同服務之間實現支付過程的事務性,這其實是在錯誤設計基礎上的僞概念)。

請注意,支付網關中的任何更改都將迫使更改多個服務,並可能更改多個團隊,因爲不同的組可以擁有這些上下文。

進行一些調整並使聚合與正確的上下文對齊,我們可以更好地表示這些子域如下圖。發生了很多變化。讓我們回顧一下更改:

通常,整體(單體)或遺留應用程序具有許多聚合,通常具有重疊的邊界。創建這些聚合及其依賴關係的上下文地圖有助於我們瞭解將要從這些整體中擺脫出來的任何新微服務的輪廓。請記住,微服務架構的成功或失敗取決於聚合之間的低耦合以及這些聚合之間的高內聚性。

同樣重要的是要注意,有界上下文本身就是合適的內聚單元。即使上下文具有多個聚合,整個上下文及其聚合也可以組成一個微服務。我們發現這種啓發式方法對於有些晦澀的領域特別有用-考慮組織正在涉足的新業務領域。您可能對分離的正確邊界沒有足夠的瞭解,並且聚集體的任何過早分解都可能導致昂貴的重構。想象一下,由於數據遷移,不得不將兩個數據庫合併爲一個,因爲我們偶然發現兩個聚合屬於同一類。但是請確保通過接口將這些聚合充分隔離,以使它們不知道彼此的複雜細節。

事件風暴-識別服務邊界的另一種技術

事件風暴是識別系統中的聚合(以及微服務)的另一種必不可少的技術。這對於破壞整體結構以及設計複雜的微服務生態系統都是有用的工具。我們已經使用這種技術分解了一個複雜的應用程序,並且打算在單獨的博客中介紹Event Storming的經驗。對於此博客的範圍,我們想給出一個快速的高級概述。如果您有興趣進一步探索,請觀看Alberto Brandelloni的視頻。

簡而言之,事件風暴是在應用程序團隊(在我們的情況下爲整體)中進行的頭腦風暴,以識別系統中發生的各種領域事件和流程。團隊還確定這些事件影響及其後續影響的彙總或模型。在團隊進行此練習時,他們會確定不同的重疊概念,模棱兩可的領域語言以及相互衝突的業務流程。他們將相關模型分組,重新定義聚合並確定重複的過程。隨着這些工作的進行,這些集合所屬的有限上下文變得清晰起來。如果所有團隊都在同一個房間(物理或虛擬)中,並開始在Scrum風格的白板上繪製事件,命令和過程的映射,那麼Event Storming研討會將非常有用。在本練習結束時,

  1. 重新定義的聚合列表。這些可能成爲新的微服務
  2. 這些微服務之間需要流動的域事件
  3. 直接從其他應用程序或用戶調用的命令

我們在一場Event Storming研討會結束時展示了一個示例板。對於團隊來說,這是一次很棒的協作活動,以就正確的聚合和有限的上下文達成一致。除了進行出色的團隊建設活動外,團隊在本次會議中脫穎而出,對領域,通用語言和精確的服務邊界有着共同的理解。

微服務之間的通信

一個整體在一個流程邊界內託管了多個聚合體。因此,在此邊界內可以管理聚合體的事務一致性,例如,如果客戶下訂單,我們可以減少項目的庫存,並向客戶發送電子郵件,全部都在一次交易事務中。所有操作都會成功,或者全部都會失敗。但是,當我們打破整體並將聚合散佈到不同的環境中時,我們將擁有數十甚至數百個微服務。迄今爲止,在整體結構的單個邊界內存在的流程現在分佈在多個分佈式系統中。要在所有這些分佈式系統上實現事務的完整性和一致性非常困難,而且要付出代價-系統的可用性。

微服務也是分佈式系統。因此,CAP定理也適用於它們- “分佈式系統只能提供三個所需特徵中的兩個:一致性,可用性和分區容限(CAP中的“ C”,“ A”和“ P”)。” 在現實世界的系統中,分區容忍度是無法商議的- 網絡不可靠,虛擬機可能宕機,區域之間的延遲會變得更糟,等等。

因此,我們可以選擇“可用性”或“一致性”。現在,我們知道在任何現代應用程序中,犧牲可用性也不是一個好主意。

圍繞最終一致性設計應用程序

如果您嘗試跨多個分佈式系統構建事務,那麼您將再次陷入困境。變成最糟糕的一種分佈式整體事務。如果任何一個系統點不可用,則整個流程將不可用,通常會導致令人沮喪的客戶體驗、失去保障失敗的承諾等。

此外,對一項服務的更改通常可能需要對另一項服務進行更改,從而導致複雜而昂貴的部署。因此,我們最好根據自己的用例來設計應用程序,以容忍一點點的不一致性,以提高可用性。對於上面的示例,我們可以使所有進程異步,從而最終保持一致。我們可以異步發送電子郵件,而與其他流程無關。

如果承諾的物品以後在倉庫中不可用,該項目可能被延期訂購,或者我們可以停止接受超過某個閾值的項目的訂單。

有時,您可能會遇到一個場景,該場景可能需要跨越不同流程邊界的兩個聚合中的強ACID樣式事務。這是重新查看這些聚合並將它們合併爲一個極好的標誌。在我們開始在不同過程邊界中分解這些聚合之前,事件風暴和上下文映射將有助於及早識別這些依賴性。將兩個微服務合併爲一個成本很高,這是我們應該努力避免的事情。

支持事件驅動的架構

微服務可能會在其聚合上發出本質上的變化。這些稱爲領域事件,並且對這些更改感興趣的任何服務都可以偵聽這些事件並在其域內採取相應的操作。這種方法避免了任何行爲上的耦合:一個域不規定其他域應該做什麼,以及時間耦合-一個過程的成功完成不依賴於同時可用的所有系統。當然,這將意味着系統最終將保持一致。

在上面的示例中,訂單服務發佈了一個事件-訂單已取消。訂閱該事件的其他服務處理其各自的域功能:付款服務退還款項,庫存服務調整項目的庫存,依此類推。要確保此集成的可靠性和彈性的幾點注意事項:

  • 生產者應確保至少生產一次事件。如果這樣做失敗,則應確保存在回退機制以重新觸發事件
  • 消費者應確保以冪等的方式消費事件。如果再次發生同一事件,那麼在用戶端不應有任何副作用。事件也可能不按順序到達。消費者可以使用時間戳記或版本號字段來保證事件的唯一性。

由於某些用例的性質,不一定總是可以使用基於事件的集成。請查看購物車服務和付款服務之間的集成。這是一個同步集成,因此我們需要注意一些事項。這是行爲耦合的一個示例-Cart服務可能從Payment服務中調用REST API,並指示其授權訂單付款,而時間耦合則需要Payment服務用於Cart服務才能接受訂單。這種耦合減少了這些上下文的自治性,並且可能減少了不希望的依賴性。有幾種方法可以避免這種耦合,但是使用所有這些選項,我們將失去向客戶提供即時反饋的能力。

  • 將REST API轉換爲基於事件的集成。但是,如果支付服務僅公開REST API,則此選項可能不可用
  • 購物車服務立即接受訂單,並且有一個批處理作業來接管訂單並調用支付服務API
  • 購物車服務會產生一個本地事件,然後調用付款服務API

在失敗和上游依賴項(付款服務)不可用的情況下,將上述內容與重試結合使用可以使設計更具彈性。例如,在發生故障的情況下,可以通過事件或基於批次的重試來備份購物車和付款服務之間的同步集成。這種方法會對客戶體驗產生額外的影響:客戶可能輸入了不正確的付款明細,並且當我們離線處理付款時,我們不會將其在線。否則,收回失敗的付款可能會增加業務成本。但是,很有可能,購物車服務對於支付服務的不可用性或故障具有彈性,其缺點勝於缺點。例如,如果我們無法離線收款,我們可以通知客戶。

避免針對特定消費者數據需求的服務之間的編排

任何面向服務的體系結構中的反模式之一是,這些服務都可以滿足消費者的特定訪問模式。通常,這發生在消費者團隊與服務團隊緊密合作時。如果團隊正在開發整體應用程序,則他們通常會創建一個跨不同聚合邊界的單一API,從而緊密耦合這些聚合。

讓我們考慮一個例子。說Web中的“訂單詳細信息”頁面,移動應用程序需要在單個頁面上同時顯示訂單的詳細信息和針對該訂單處理的退款的詳細信息。在整體應用程序中,Order GET API(假設它是REST API)一起查詢Orders和Refunds,合併兩個聚合,然後將複合響應發送給調用方。由於聚合屬於相同的過程邊界,因此無需太多開銷即可執行此操作。因此,消費者可以在一個調用中獲得所有必要的數據。

如果訂單和退款是不同上下文的一部分,則數據不再存在於單個微服務或聚合邊界內。爲消費者保留相同功能的一種選擇是使訂單服務負責調用退款服務並創建複合響應。此方法引起以下問題:

  1. 訂單服務現在與另一個服務集成在一起,純粹是爲了支持需要退款數據和訂單數據的消費者。現在,訂單服務的自治性降低了,因爲退款總額中的任何更改都將導致訂單總額中的更改。
  2. 訂單服務具有另一個集成,因此要考慮另一個故障點-如果退款服務出現故障,訂購服務是否仍可以發送部分數據,並且消費者可以正常地故障嗎?
  3. 如果消費者需要更改以從“退款”聚合中獲取更多數據,則現在需要兩個團隊來進行更改
  4. 如果在整個平臺上都遵循這種模式,則可能導致各種域服務之間的依存關係錯綜複雜,所有這些都是因爲這些服務滿足了調用者的特定訪問模式。

前端的後端BFF

減輕這種風險的一種方法是讓消費者團隊管理各種域服務之間的編排。畢竟,呼叫者會更好地瞭解訪問模式,並且可以完全控制對這些模式的任何更改。這種方法將域服務與表示層分離開來,使它們專注於核心業務流程。但是,如果Web和移動應用程序開始直接調用不同的服務而不是從整體中調用一個複合API,則可能會導致這些應用程序的性能開銷–通過較低帶寬網絡進行多次調用,處理和合並來自不同API的數據,等等。 。

相反,可以使用另一種稱爲前端的後端的模式。在這種設計模式下,由消費者(在本例中爲Web和移動團隊)創建和管理的後端服務負責跨多個域服務的集成,純粹是爲了向客戶提供前端體驗。Web和移動團隊現在可以根據他們的用例設計數據合同。他們甚至可以使用GraphQL而不是REST API來靈活地查詢並準確獲取所需的信息。

重要的是要注意,此服務是由使用者團隊擁有和維護的,而不是由擁有域服務的團隊擁有和維護的。前端團隊現在可以根據自己的需求進行優化-移動應用程序可以請求更小的有效負載,減少來自移動應用程序的呼叫次數等等。查看下面的業務流程的修訂視圖。

結論

在此博客中,我們觸及了各種概念,策略和設計啓發法,以便在我們進入微服務領域時,尤其是在嘗試將整體式服務拆分爲基於域的微服務時,加以考慮。其中許多主題本身就是廣闊的主題,我認爲我們沒有做出足夠的公正性來詳細解釋它們,但是我們想介紹一些關鍵主題以及我們採用這些主題的經驗。進一步閱讀(鏈接)部分提供了一些參考資料和一些有用的內容,供任何希望採用此方法的人使用。

原文:https://medium.com/walmartglobaltech/building-domain-driven-microservices-af688aa1b1b8
譯文:jdon.com/54558

近期熱文推薦:

1.1,000+ 道 Java面試題及答案整理(2022最新版)

2.勁爆!Java 協程要來了。。。

3.Spring Boot 2.x 教程,太全了!

4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這纔是優雅的方式!!

5.《Java開發手冊(嵩山版)》最新發布,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章