關於如何設計一個基於事件驅動架構的思考

tangxuehua發表於2013-03-29
最近一直在思考一個問題:有沒有這樣一種可能,就是一個領域模型的狀態不依賴於外部,它只負責接收外部的事件,然後根據這些事件做出響應;響應分兩種:
1)根據模型當前的記憶體狀態進行業務邏輯處理,然後產生事件,注意:這個過程不會改變模型當前的記憶體狀態;
2)根據事件改變自己的狀態;

另外,也是最重要的,領域模型不用關心自己所產生的事件到底怎麼樣了,比如不關心有沒有持久化,不關心是否和別的事件有併發衝突。它只管根據自己當前的記憶體狀態做上面這兩點的響應;

如果這樣的設想有可能,那領域模型就是真正的中央業務邏輯處理器了,和CPU很類似了。這樣它才能真正快起來。

簡單的說就是:事件->模型->事件
模型只管響應事件,然後響應處理,然後產生新的事件

領域模型就是一黑盒,它只能幫你處理業務邏輯,其他的什麼處理結果它一概不關心;當然,領域模型肯定有它自己的狀態,但這個狀態是駐留在記憶體的,和領域模型是一體的。

我為什麼會有這個想法是因為,我在想,為什麼要讓領域模型的處理邏輯依賴於它的處理結果是否被正確順利持久化了?感覺這很荒唐。
既然領域模型有自己的記憶體狀態空間,他的所有邏輯也應該只依賴於這個狀態空間,不再依賴於其他任何外部的東西。

當然,以前我們設計的IRepository,實際背後都是直接從資料庫取。這樣的話,領域模型的狀態空間就是資料庫了。但是這樣其實很不好,為什麼不用記憶體作為領域模型的狀態空間呢?

現在再想想LMAX就是我剛才的想法的一個實際例子。

事件->模型->事件,這樣的設計,理論上並不需要必須要求單執行緒來訪問模型,因為領域模型不依賴於任何外部的狀態,只依賴於自己所在存活記憶體空間;單執行緒有一個很大的好處就是可以防止併發衝突的產生。我們其實完全支援多執行緒或叢集的方式,只不過這樣會有可能訪問到的領域物件的狀態是了老的,因為不同的機器之間的領域模型記憶體物件的狀態需要做一些同步,訪問到老資料的可能性的大小取決於併發的大小以及機器之間資料同步的快慢;
LMAX之所以用單執行緒,是考慮了,這單執行緒的領域模型和效能之間,效能已經非常高其足以達到他們的要求了。

這樣的架構,我覺得領域模型中的任何一個物件的一次完整的狀態更新至少會響應兩個事件,舉個例子:

1)先響應ChangeNoteCommand(command也是一種事件,可以理解為NoteChangeRequested),然後Note模型產生一個NoteChanged事件,注意,此時模型自己的狀態還未改變,此時只是先產生了一個事件表示什麼事情發生了;
2)然後該事件(NoteChanged)最終又被髮送到領域模型讓其響應,此時,領域模型才去更改自己的Note狀態並將最新狀態儲存到自己的記憶體空間,如一個dict中或redis中;

經過對這兩個事件的響應,才完成了Note的最終狀態的修改;而我們以前都是從資料庫取Note,然後更改,然後儲存到資料庫。這樣不慢才怪!

透過上面的兩次事件響應,可以換來領域模型對事件的極快的響應,因為完全無IO。
剩下的我們只要考慮(我目前考慮了以下六個問題):
1.訊息的序列化和反序列化;
2.訊息傳遞的速度;
3.事件持久化的速度;
4.併發衝突後重試的設計;
5.訊息丟失了怎麼辦;
6.叢集部署時,各臺伺服器之間記憶體的同步如何實現;

需要明白的是:這些都不是領域模型該考慮的問題。這些外圍的任何問題,都不要讓領域模型自己去考慮,我們應該對出現的各種問題逐個尋求解決方案。

每個問題的解決方案我大概理了下我的對策:
1.訊息的序列化和反序列化:這個簡單,用BinaryFormatter,或更快的開源序列化元件,對於事件這樣大小的物件可以達到每秒10W次每秒;
2.訊息傳遞的速度:用MSMQ/RabbitMq,等帶持久化功能的佇列元件;如果嫌太慢,就用ZeroMq(無訊息持久化功能),但可以達到30W訊息每秒;
3.事件持久化的速度:由於事件都是跟著單個聚合根,所以我們只要確保單個聚合根的事件不會衝突(即沒有重複的版本號的事件);為了更快的持久化,我們可以對事件按照聚合根或者其他方式進行分割槽存放,不同的伺服器存放不同的聚合根的事件;這樣透過叢集持久化的方式可以實現多事件同時被持久化,從而提高整體的事件持久化吞吐量;如單個mongodb server每秒持久化5000個,那10個mongodb server就能每秒持久化5W個;
4.併發衝突後怎麼辦:一般來說就是選擇重試,但為了確保不會出現不可控的局面(可能由於某種原因一直在重試,引起訊息堵塞),那需要設定一個最大的重試次數;超過最大重試次數後不再重試,然後記錄日誌,以供以後查詢問題;這裡的重試的意思是:重新找到對應該事件的command,然後再次傳送該command給領域模型處理;
5.訊息丟失:丟失就丟失了唄,呵呵;要是你覺得訊息決不能丟失,那就用可靠的帶持久化功能的訊息傳輸佇列,如MSMQ;當然,就算訊息丟失了,我們很多時候都要想想有沒有影響的,一般來說,訊息丟失,至少我們是知道程式有問題了的,因為模型的狀態此時一定是不對的。我們可以透過在訊息發出時和接收時記錄日誌,這樣方便以後查詢訊息是在哪個環節丟的;
6.任何其他的異常出現,這個我覺得如果都是託管程式碼,那可以在必要的地方加try catch,然後記錄日誌。至於是否要重試,還要看情形;
7.另外,如果是多執行緒訪問模型,或叢集訪問,那很多時候訪問到的記憶體的領域物件的狀態都是老的,那怎麼辦?其實這不是問題,因為事件持久化的時候會被檢測到這種併發重複,然後對應的command會被重試。
8.如果一個事件被成功的持久化了,那如何讓各臺應用伺服器知道?這個我覺得也簡單,就是當事件持久化完成後,透過zeromq publish給所有的應用伺服器,每臺應用伺服器都有一個後臺的執行緒在不停的接收已被成功持久化了的事件,然後根據這些事件更新自己記憶體空間中的領域物件的狀態。這一步完全可以由框架自動做掉;這裡相當於我上面提到的第二個事件(NoteChanged)是由框架自動處理的,不需要使用者寫程式碼干預;前面說到,因為是publish-subscribe模式,所以各臺應用伺服器上的資料就會自然保持同步了;

另外,這種架構,傳輸的是事件,事件都是很小的,所以不用擔心訊息傳輸的效能。

目前就想到這些。後續再完善思路。

最後,我一直認為:知識決定命運,學習積累知識,而正確的思維方式是一切高效學習的基礎。所以要學會如何清晰地思考!

banq,技術無國界,無平臺差別,最重要的是思路,我是搞.net,你是搞java,但我覺得只要我說的不是.net特有的東西,你們一定也能理解對嗎?

呵呵!

相關文章