1. 程式人生 > 其它 >DDD 領域驅動設計學習(四)- 架構(CQRS/EDA/管道和過濾器)

DDD 領域驅動設計學習(四)- 架構(CQRS/EDA/管道和過濾器)

原文:

https://www.jianshu.com/p/edd8db46ea99?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

命令和查詢職責分離(CQRS)

做過應用系統的人對系統開發的內容應該有一個感覺,系統後端的主要工作包括CRUD (Create, Read, Update, Delete) 增查改刪;業務邏輯和大量查詢。大部分的軟體研發的架構和方法論主要基於業務邏輯方面,而且通常查詢的業務量要遠遠大於做資料變更的業務量,CRUD和查詢如果採用複雜的架構反而感覺有點過重了。
CQRS也符合關注點分離的思想,看到CQRS的邏輯檢視,第一感覺跟自己以前做過的一個系統中把所有查詢單獨做了一個動態查詢元件,開發者只需要定義查詢,寫入對應的SQL語句,這個元件就會完成呼叫後臺服務傳輸到前臺的所有工作,這個元件大大簡化了開發查詢服務的開發效率。
CQRS架構本身只是一個讀寫分離的思想,如下圖。


CQRS

實現方式多種多樣,比如資料儲存不分離,僅僅只是程式碼層面讀寫分離,這個就是我們以前曾經做過的方式;另外資料儲存的讀寫分離,C端負責資料儲存,Q端負責資料查詢,Q端的資料通過C端產生的Event來同步,可以看成是讀寫分離和Event Sourcing的一個結合體。CQRS的原理上看很完美,不過在Martin Fowler的文章中,對CQRS的實現複雜性提出了告警:“ Despite these benefits, you should be very cautious about using CQRS.”
個人的看法,大型應用系統採用讀寫分離本身就是一種常見的方案。而在某些場景中,使用傳統的資料庫技術也能夠解決同樣的問題,並且要簡便的多。對於C部分所採用的事件驅動方式,這個跟傳統軟體開發模式有不少差異。在一些複雜的應用場景中可能有不少問題需要進行重新設計。
Dahan認為在某一種場景下是非常適合應用CQRS架構的,即具有高競爭性的業務領域。在這種領域中的負載非常大,而且具有高度的局域性。

CQRS/ES

事件驅動架構

EDA事件驅動架構首先不是對於傳統的面向業務流程,資料等各種架構模式的完全否定,而是解決傳統架構下無法很好解決的一些問題。傳統模式裡面更加關注業務流程和業務物件,而EDA模式下將更加關注在整個業務流程中的關鍵狀態點,已經由關鍵狀態點觸發的有明確業務含義的業務事件。

在《實現領域驅動設計》一書中提出採用EDA結合六邊形架構的思路。 EDA和六邊形架構

一個系統的輸出埠所發出的領域事件將被髮送到另一個系統的輸人埠,此後輸人埠的事件訂閱方將對事件進行處理。對於不同的BC來說,不同的領域事件具有不同含義。在一個BC處理某個事件時,應用程式 API將採用該事件中的屬性值來執行相應的操作。
在一個多工處理過程中,如果這個事務只有在所有的參與事件都得到處理之後,我們才能認為這個多工處理過程完成了,某種領域事件只能表示該過程中的一部分。這時候這個過程的處理就需要結合其他架構進行處理。如果處理事件和訊息在下面章節做介紹。

管道和過濾器(Pipes And Filters)

管道過濾器和生產流水線類似,在生產流水線上,原材料在流水線上經一道一道的工序,最後形成某種有用的產品。在管道過濾器中,資料經過一個一個的過濾器,最後得到需要的資料。
通過標準化每個元件接收和發射的資料的格式,這些過濾器可以組合在一起成為一個管道。這有助於避免重複程式碼,並且可以很容易地移除,替換或整合額外的元件。
看一個Linux下的Shell命令,該命令用於在 phone_numbers.txt檔案中統計含有電話區號“303"的所有文字行的數量。

 $ cat phone_numbers.txt | grep303 | wc -l  

在以上的命令工具中,每個工具都接收一個數據集,對其進行處理,再輸出另一個數據集。輸出資料集和輸人資料集是不同的,因為每一個命令都充當著過濾器的作用。在整個過濾過程完成之後,輸出資料和輸人資料可能完全不一樣了。在本例中,最原始的輸人是一個文字檔案,但最終的輸出則只有一個數字“3”。

但如何將上面的基本原則用於事件驅動架構呢?如下圖所示: 事件驅動

長時間處理過程(Saga)

對上面的管道和過濾器的例子進行擴充套件,可以得到另一種事件驅動的分散式的並行處理模-----長時處理過程(Long-Running Process)。一個長時處理過程有時也稱為Saga。

長時處理過程
和先前的例子不同的是,此時的長時處理過程將由PhoneNumberExecutive(下面簡稱PNE)來啟動,同時它還將對處理過程進行跟蹤。 PNE可以重用 PhoneNumbersPubIisher,也可以不再重用。PNE可以通過應用服務或者命令處理器的形式實現,它將跟蹤長時處理過程的各個階段。同時PNE它還知道一個長時處理過程何時執行完畢,並在這些過程執行完畢之後,再執行其他任務。
然而,這個例子中還存在一個問題。PNE無法知道所接收到的兩個領域事件是否來自同一個並行處理過程。處理過程並行啟動,完成事件無序地產生,那麼 PNE如何知道是哪個處理過程執行完畢了呢?在電話號碼統計這個例子中,出現這個問題可能並不嚴重。但是,當處理真實的企業業務領域時,這樣的問題卻有可能是災難性的。
解決這個問題的第一步是在每個領域事件中加人處理過程的身份標識。這個標識可以和引發處理過程的領域事件的標識相同,比如 AllPhoneNumbersListed事件。我們可以使用 UUID。 PNE只有在接收到具有相同標識的領域事件時才會輸出日誌記錄。然而PNE並不需要等待所有事件的到達,它也是一個事件訂閱方,在事件到達時將自動啟動相應的處理過程。
在實際的領域中,一個長時處理過程的執行器將建立一個新的類似聚合的狀態物件來跟蹤事件的完成情況。該狀態物件在處理過程開始時建立,它將與所有的領域事件共享一個唯一標識。長時處理過程的狀態物件下圖所示。
狀態物件
當並行處理的每個執行流執行完畢時,執行器都會接收到相應的完成事件。然後,執行器根據事件中的過程標識獲取到與該過程相對應的狀態跟蹤物件例項,再在這個物件例項中修改該執行流所對應的屬性值。
當與遺留系統的整合存在很大的時間延遲時,採用長時處理過程將非常有用。即便時間延遲和遺留系統並不是我們的主要關注點,我們依然能從長時處理過程中得到好處,即由分散式和並行處理所帶來的優雅性。這樣也有助於我們開發高可伸縮性、高可用性的業務系統。

事件源

一個物件從建立開始到消亡會經歷很多事件,以前我們是在每次物件參與完一個業務動作後把物件的最新狀態持久化儲存到資料庫中,也就是說我們的資料庫中的資料是反映了物件的當前最新的狀態。而事件溯源則相反,不是儲存物件的最新狀態,而是儲存這個物件所經歷的每個事件,所有的由物件產生的事件會按照時間先後順序有序的存放在資料庫中。 Event Sourcing

那麼,事件到底如何影響一個領域物件的狀態的呢?Event sourcing事件溯源是借鑑資料庫事務日誌的一種資料持久方式,在事務日誌中記錄導致狀態變化的一系列領域事件。通過持久化記錄改變狀態的事件,通過重新播放獲得狀態改變的歷史。 事件回放可以返回系統到任何狀態,這個過程就是所謂的事件溯源。
另一方面,由於事件流本身具有邏輯上嚴格次序性,因此使用統一的事件流(事務日誌)能夠很自然實現事務機制,無需額外ACID機制或2PC之類同步強硬方式。
我們可以看到基於這樣的設計,領域物件的狀態完全是由事件驅動的。不僅如此,事件還可以被事件匯流排分發出去,通知領域模型外的一切事件響應者發生了什麼,基於這種Publish-Subscribe的通訊模式,我們可以最大限度的實現系統的鬆耦合。

參考資料
1. 深度長文:我對CQRS/EventSourcing架構的思考
2. DDD CQRS架構和傳統架構的優缺點比較
3. 對CQRS的一次批判性思考
4. 再談EDA事件驅動架構
5. Reference 6: A Saga on Sagas