對LMAX架構以及Event Sourcing模式的一些新思考和問題的記錄


最近又學習了一下LMAX架構,讓我對該架構以及event sourcing模式又有了很多新的認識和疑問。

注:如果不知道什么是lmax架構和event sourcing模式的看官可以自己先去查查資料:

  • LMAX可以看看martin寫的一篇文章:http://martinfowler.com/articles/lmax.html
  • Event Sourcing的資料比較多,隨便google一下即可。
  • 當然,我的博客里也有大量關於這兩個方面的筆記,有興趣的可以看看。

下面是我的一些最新的想法。

LMAX architecture:input event + business logic processor(BLP) + output event

架構主要執行過程:
首先input event由上層(如controller)創建並最后統一匯集到input disruptor(一個並發控制組件),然后BLP在單個線程內處理所有的input event,一般處理的情況有:1)簡單時,直接讓aggregate 處理,處理完之后aggregate會產生output event;2)如果是復雜的過程,如long running process,則通過saga的方式來控制整個業務流程;首先也是由aggregate來處理input event,然后產生的output event會由saga響應,然后saga會根據流程邏輯決定接下來要做什么,即產生哪個input event;實際上我把saga也看成是一種聚合,因為saga也有狀態,saga表達了一個流程的處理狀態,saga也有唯一標識,saga也需要被持久化;總之,BLP在處理完input event后會產生output event。然后這些output event會被某些關心的event handler處理;另外有些event handler在處理output event時又會產生另外的input event並最終也發送到input disruptor,整個過程大概是這樣。不知我理解的是否正確。

下面針對我上面的理解再做一些總結:

  1. 整個過程有下面這幾個主要元素構成:input event + BLP(包含aggregate,saga) + output event;
  2. input event,output event用於消息(message)傳遞,實際上他們都屬於消息,並且也都是domain event?
  3. BLP用於處理業務邏輯(由aggregate負責)和流程控制邏輯(由saga負責);
  4. aggregate產生output event,output event會最終被發送到output disruptor;
  5. output event有兩個主要作用:1)可以讓領域外知道領域內發生了什么;2)可以通過output event串聯某些復雜的業務過程,如銀行轉賬,如提交訂單,etc;
  6. 值得注意的是:整個BLP(saga+aggregate)是in-memory的,重建BLP是用input event來實現,而不是output event;這也是為什么LMAX架構中在BLP處理input event之前必須先通過一個叫journaler的東西持久化input event的原因。目的就是為了在需要的時候利用這些input event通過event sourcing(事件溯源)模式重建整個BLP。其實這個行為更直白的理解就是讓BLP再重新處理一遍所有的input event;當然,在重建過程中對於任何要訪問外部系統接口的地方,都要禁止訪問,否則會帶來問題,尤其是更新外部系統的時候,這個其實比較簡單,只要設計一個gateway即可,重建blp的時候設置一下該gateway即可。

接下來我想闡述一些我覺得自己比較糾結的地方:

event sourcing的中文解釋是事件溯源,關鍵是如何理解溯源?我的理解是:根據已經發生的事情來重現歷史。如果這個理解是正確的,那何為已經發生的事情?lmax是通過input event來溯源,也就是說Lmax認為已經發生的事情是input event,而非output event,即LMAX認為已經發生是指只要input event一旦被創建就表示事情已經發生了,即已經發生是針對用戶而言的,如用戶提交了訂單,那就是OrderSubmitted,用戶點擊了修改資料的保存按鈕,那就是UserProfileChangeRequested;而我們之前的做法是根據aggregate產生的output event來溯源,即我們認為已經發生是相對aggregate而言的;那么到底哪種思路更好呢?雖然兩種做法都能最終還原BLP。但就我個人理解,我覺得lmax的做法更合理,實際上如果讓LMAX和CQRS架構的command端做對比,那么input event相當於command,只不過command一般都是動詞,所以就是CreateOrder,ChangeUserProfile。所以可以理解為lmax架構實際上是在replay command;所以問題就演變為我們到底應該replay command還是replay event?想想replay是誰在replay?是聚合根,這點毫無疑問。另外,replay從語義上來說實際上就是和play做的事情是一樣的,只不過是“重做”的意思。那么要理解重做首先要理解什么是“做”?我對“做”的理解就是執行行為並改變狀態。所以“重做”就是重新重新執行行為並改變狀態;replay command相當於是在重做別人給aggregate一些命令;而replay event相當於是在重做aggregate自己以前曾今做過的一些事情。其實,最重要的一點是,到底要重做什么?是重做用戶的要求(what user want to do)還是重做聚合根內已經發生的事情(what domain has happened.),這個問題的回答直接決定到底該replay command 還是 replay event,呵呵。所以,按照這樣的思路來思考就很明顯了,LMAX是在重做用戶的要求,而我們之前的replay event則是在重做聚合根內已經發生的事情。如果我認為重做應該是重做用戶的要求,那replay event就不是真正意義上的重做了,而僅僅只是改變狀態。舉例:假設有一個Note聚合根,有一個ChangeTitle的公共方法,然后還有一個ChangeTitleCommand,ChangeTitleCommand的handler會調用Note的ChangeTitle方法;另外Note還有一個OnTitleChanged的私有方法,用於響應TitleChanged事件。如果是replay command,那會導致ChangeTitle會被重新調用,這就是重做用戶的要求;而如果是replay event,則只有OnTitleChanged方法被重新調用,也就是說只是在重做聚合根內已經發生的事情。思考到這里,我不得不承認第一個思考出這種思路的人很厲害,因為他用了這種繞個彎的做法(將本來可以放在一個方法內一次性完成的任務(先改狀態然后再產生output event))拆分為兩個步驟,第一步是先僅僅產生一個TitleChanged的事件,第二步才是響應該事件並作出狀態改變。這樣拆分的目的是可以讓第二步的方法(OnTitleChanged方法)可以用於event sourcing。另外,這兩步對聚合根外部來說是透明的,因為外部根本不知道內部是通過兩個步驟來實現的。不得不承認這種做法在replay的時候遠比replay command要容易的多,因為所有的aggregate的內部事件響應函數都不會涉及與任何外部系統的交互。雖然這種做法挺好,但是我覺得我們非常有必要搞清楚這兩種不同的event sourcing的區別。

另一方面,從確保event必須被持久化的角度來講:我覺得LMAX的架構,即replay command的好處是,可以很容易在進入BLP之前持久化command,真正做到在BLP處理之前確保所有事件已經被持久化了;而如果是replay event,那我們就沒辦法實現一個in-memory的BLP了,因為首先BLP是in-memory的,即沒有任何IO,但是我們又要求必須持久化output event。那怎么辦呢?如果是同步的方式持久化output event,那就不是in-memory了;如果是異步的方式來持久化output event,那雖然可以做到in-memory,但怎么確保output event一定已經被持久化了呢?

目前就這些了,以后有更多的思考內容再補充上來。

-------------------------------------------------------------------------------------------------------------------------------

后來又做了一些思考。

想來想去,最終還是傾向於應該通過output event來做event sourcing。因為畢竟只有output event才真正表示domain aggregate認可的可以發生的domain event。而我們要重建的就是聚合根,到底是應該通過重復執行用戶的命令來讓模型達到最新狀態還是通過讓聚合根重新執行已經發生過的事情呢?現在想來,應該是后者。雖然前者也可以,但是要付出的代價相對比較大,比如重建時要禁用外部系統的調用,最麻煩的還是重啟發布時的很多細節問題要考慮;而通過聚合根已經發生的事情來重建,則相對很容易,因為重建時不會涉及任何模型之外的東西!

但是因為我們現在采用了in-memory domain的架構,所以傳統的基於數據庫事務的做法已經無法使用了。所以需要設計另外一種架構確保在domain修改狀態之前domain event已經被持久化了,為什么要做這個保證是因為event sourcing+in memory的架構實際上是一種event driven architecture,即整個領域模型的狀態的修改都是由事件驅動的,這意味着如果要改變內存中的領域模型的狀態,那必須先確保引起該狀態修改的domain event必須已經被持久化了。

從用戶發起一個command后執行的流程如下:disruptor是並發控制組件,大家可以暫時理解為一個消息隊列。如果要進一步了解disruptor,可以看看LMAX架構。

  1. Send ChangeNoteTitleCommand to input disruptor;
  2. Command handler execute method called by input disruptor;
  3. Note.ChangeTitle method called by command handler execute method;
  4. NoteTitleChanged domain event is created and raised in note.ChangeTitle method;
  5. The infrastructure framework send the above NoteTitleChanged event to input disruptor when the raise method is called;
  6. Journal event handler called by input disruptor to persist the event;
  7. Another event handler called by input disruptor to really apply all the note state changes according with the event.
  8. A third event handler called by input disruptor to send the event to the output disruptor;
  9. All the external event handlers are called by the output disruptor; for example, some external event handlers will update the CQRS query side data;

以上步驟必須嚴格按照上面的順序一步步執行下來,否則無法確保邏輯正確。
另外,以上流程目前只考慮單台機器,未考慮主備或集群的架構如何實現;之所以用英文寫是因為我還要拿去和老外討論,呵呵。不過這幾句英文應該比較簡單吧。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM