最近一直在思考一個問題:有沒有這樣一種可能,就是一個領域模型的狀態不依賴於外部,它只負責接收外部的事件,然后根據這些事件做出響應;響應分兩種:
- 根據模型當前的內存狀態進行業務邏輯處理,然后產生事件,注意:這個過程不會改變模型當前的內存狀態;
- 根據事件改變自己的狀態;
另外,也是最重要的,領域模型不用關心自己所產生的事件到底怎么樣了,比如不關心有沒有持久化,不關心是否和別的事件有並發沖突。它只管根據自己當前的內存狀態做上面這兩點的響應;
如果這樣的設想有可能,那領域模型就是真正的中央業務邏輯處理器了,和CPU很類似了。這樣它才能真正快起來。
簡單的說就是:事件->模型->事件
模型只管響應事件,然后響應處理,然后產生新的事件
領域模型就是一黑盒,它只能幫你處理業務邏輯,其他的什么處理結果它一概不關心;當然,領域模型肯定有它自己的狀態,但這個狀態是駐留在內存的,和領域模型是一體的。
我為什么會有這個想法是因為,我在想,為什么要讓領域模型的處理邏輯依賴於它的處理結果是否被正確順利持久化了?感覺這很荒唐。
既然領域模型有自己的內存狀態空間,他的所有邏輯也應該只依賴於這個狀態空間,不再依賴於其他任何外部的東西。
當然,以前我們設計的IRepository,實際背后都是直接從數據庫取。這樣的話,領域模型的狀態空間就是數據庫了。但是這樣其實很不好,為什么不用內存作為領域模型的狀態空間呢?
現在再想想LMAX就是我剛才的想法的一個實際例子。
事件->模型->事件,這樣的設計,理論上並不需要必須要求單線程來訪問模型,因為領域模型不依賴於任何外部的狀態,只依賴於自己所在存活內存空間;單線程有一個很大的好處就是可以防止並發沖突的產生。我們其實完全支持多線程或集群的方式,只不過這樣會有可能訪問到的領域對象的狀態是了老的,因為不同的機器之間的領域模型內存對象的狀態需要做一些同步,訪問到老數據的可能性的大小取決於並發的大小以及機器之間數據同步的快慢;
LMAX之所以用單線程,是考慮了,這單線程的領域模型和性能之間,性能已經非常高其足以達到他們的要求了。
這樣的架構,我覺得領域模型中的任何一個對象的一次完整的狀態更新至少會響應兩個事件,舉個例子:
- 先響應ChangeNoteCommand(command也是一種事件,可以理解為NoteChangeRequested),然后Note模型產生一個NoteChanged事件,注意,此時模型自己的狀態還未改變,此時只是先產生了一個事件表示什么事情發生了;
- 然后該事件(NoteChanged)最終又被發送到領域模型讓其響應,此時,領域模型才去更改自己的Note狀態並將最新狀態保存到自己的內存空間,如一個dict中或redis中;
經過對這兩個事件的響應,才完成了Note的最終狀態的修改;而我們以前都是從數據庫取Note,然后更改,然后保存到數據庫。這樣不慢才怪!
通過上面的兩次事件響應,可以換來領域模型對事件的極快的響應,因為完全無IO。
剩下的我們只要考慮(我目前考慮了以下六個問題):
- 消息的序列化和反序列化;
- 消息傳遞的速度;
- 事件持久化的速度;
- 並發沖突后重試的設計;
- 消息丟失了怎么辦;
- 集群部署時,各台服務器之間內存的同步如何實現;
需要明白的是:這些都不是領域模型該考慮的問題。這些外圍的任何問題,都不要讓領域模型自己去考慮,我們應該對出現的各種問題逐個尋求解決方案。
每個問題的解決方案我大概理了下我的對策:
- 消息的序列化和反序列化:這個簡單,用BinaryFormatter,或更快的開源序列化組件,對於事件這樣大小的對象可以達到每秒10W次每秒;
- 消息傳遞的速度:用MSMQ/RabbitMq,等帶持久化功能的隊列組件;如果嫌太慢,就用ZeroMq(無消息持久化功能),但可以達到30W消息每秒;
- 事件持久化的速度:由於事件都是跟着單個聚合根,所以我們只要確保單個聚合根的事件不會沖突(即沒有重復的版本號的事件);為了更快的持久化,我們可以對事件按照聚合根或者其他方式進行分區存放,不同的服務器存放不同的聚合根的事件;這樣通過集群持久化的方式可以實現多事件同時被持久化,從而提高整體的事件持久化吞吐量;如單個mongodb server每秒持久化5000個,那10個mongodb server就能每秒持久化5W個;
- 並發沖突后怎么辦:一般來說就是選擇重試,但為了確保不會出現不可控的局面(可能由於某種原因一直在重試,引起消息堵塞),那需要設置一個最大的重試次數;超過最大重試次數后不再重試,然后記錄日志,以供以后查找問題;這里的重試的意思是:重新找到對應該事件的command,然后再次發送該command給領域模型處理;
- 消息丟失:丟失就丟失了唄,呵呵;要是你覺得消息決不能丟失,那就用可靠的帶持久化功能的消息傳輸隊列,如MSMQ;當然,就算消息丟失了,我們很多時候都要想想有沒有影響的,一般來說,消息丟失,至少我們是知道程序有問題了的,因為模型的狀態此時一定是不對的。我們可以通過在消息發出時和接收時記錄日志,這樣方便以后查找消息是在哪個環節丟的;
- 任何其他的異常出現,這個我覺得如果都是托管代碼,那可以在必要的地方加try catch,然后記錄日志。至於是否要重試,還要看情形;
- 另外,如果是多線程訪問模型,或集群訪問,那很多時候訪問到的內存的領域對象的狀態都是老的,那怎么辦?其實這不是問題,因為事件持久化的時候會被檢測到這種並發重復,然后對應的command會被重試。
- 如果一個事件被成功的持久化了,那如何讓各台應用服務器知道?這個我覺得也簡單,就是當事件持久化完成后,通過zeromq publish給所有的應用服務器,每台應用服務器都有一個后台的線程在不停的接收已被成功持久化了的事件,然后根據這些事件更新自己內存空間中的領域對象的狀態。這一步完全可以由框架自動做掉;這里相當於我上面提到的第二個事件(NoteChanged)是由框架自動處理的,不需要用戶寫代碼干預;前面說到,因為是publish-subscribe模式,所以各台應用服務器上的數據就會自然保持同步了;
另外,這種架構,傳輸的是事件,事件都是很小的,所以不用擔心消息傳輸的性能。
對於以上的想法,有人有下面的兩點擔心:
- 事件是否就是解決當前復雜軟件架構的銀彈?
- 系統中如果出現海量的事件是否會出現另一種災難?
我記得不知道是誰說過,OO的本質就是消息通信。command也好,event也好,或者直接的方法調用也好,本質上都是對象與對象之間的消息通信。
方法調用太生硬(這點我記得你曾今也提到過,當然我覺得聚合內很適合用方法調用來實現聚合內的對象的通信)
command, event本質上都是通過message作為媒介,實現對象與對象之間的通信。這讓我想起有一位高人曾經說過的一個比喻,下面是摘錄的他的原話:
“現在的SOA、ESB之類的東西是不是就像打造一個企業的“神經脈絡”,而“OO”是不是就像“神經元”,它們之間的通訊就是靠生物電脈沖,這就是消息驅動。”
所以,我在想,軟件實現用戶的需求,是不是也應該有很多的對象以及很多的消息(event)這兩樣東西作為核心組成,對象相當於神經元,消息相當於生物電脈沖。整個軟件在運行過程中就是這樣一個由對象以及消息組成的網絡。
至於復雜性,我覺得框架可以幫我們實現消息通信的部分,而我們程序員要做的就是定義對象結構,然后讓對象具有發送消息和接收消息的行為功能。我覺得這點並不是很復雜吧!
最近我一直在努力實現我這個想法,因為我師兄說:“我現在不相信什么架構,just show me the code”。
有想法和能實現出來是兩回事,你有多少能力,你的設計能力,對細節的把控能力,程序員內在素養,一看代碼便知,呵呵。
有人回復說:事件本身沒有錯,我想強調的是“事件”的定位問題。“事件”是一個界與另一個界交互的方式,但界是分層次的。用人體比喻很好理解,細胞之間的事件,組織之間的事件,器官之間的事件。構建這樣的事件體系是非常復雜的,目前的技術很難達到,不是一個EventBus就可以解決的。
針對上面的說法,我覺得這里主要還是一個編程思路的轉變問題。事件驅動天生是一種異步編程。我之所以想自己搞一個這樣的框架,主要是因為:
- 事件驅動的編程模型讓model不在有任何負擔,讓model只面向in memory,從而實現高性能不是夢了;
- 事件的version機制讓我們方便的實現樂觀並發,確保單個聚合根內強一致,聚合根之間最終一致;然后配合框架自動實現的重試功能,可以在並發沖突后自動重試,這樣極大避免command的執行失敗率;
- 事件數據不是關系型數據,所以事件產生者和處理者都可以多個,這意味着我們做集群非常容易,且事件的存儲可以任意拆分,只要確保同一個聚合根的事件放在一起即可,不同聚合根的事件理論上都可以放在不同的服務器上,這樣我們持久化事件也可以並發,我們只要對聚合根id+commitSequence這兩個字段建立唯一索引即可。從而克服事件持久化(IO操作)慢的瓶頸;
在這么多誘人的特性面前,我們還有什么說不的理由呢?困難不要緊,我們可以一步步來,呵呵。總比沒有想法好,你說呢?