當提到“事件驅動”時,我們在說什麼?

ThoughtWorks發表於2019-03-13

當提到“事件驅動”時,我們在說什麼?

文/Martin Fowler

譯/梅雪松

去年年底(譯者注:2016年底),我和ThoughtWorks同事一起參加了一個研討會,討論“事件驅動”的本質。在過去的幾年裡,我們構建的很多系統都大量使用了事件。對於這些系統,人們常常讚譽有加,但批評的聲音也不絕於耳。我們的北美辦公室組織了一次峰會,來自世界各地的ThoughtWorks資深開發者出席會議並分享了他們的想法。

這次峰會的最大認識是到當人們談論“事件”時,實際上說的是完全不同的東西,所以我們花了很多時間來梳理一些有用的模式。本文簡要總結我們的成果。

事件通知

當領域內有變化發生時,傳送事件訊息來通知其它系統。事件通知的一個關鍵點是源系統並不關心外部系統的響應。通常它根本不期待任何結果,即使有也是間接的。 傳送事件的邏輯流與響應該事件的邏輯流之間會有顯著的隔離。

事件通知非常有用,因為它意味著低耦合,並且結構也非常簡單。但是,當邏輯處理流跨越各種事件通知時,它也可能成為問題。因為沒有任何程式碼顯式地描述這個流程,所以這個流程是不可見的。通常,唯一的辦法是通過監控系統來觀察它。這會導致除錯和修改流程變得很困難。這裡的危險在於,當你使用事件通知來優雅地做系統解耦時,沒有意識到更大規模的流程,而這會讓你在未來幾年中陷入困境。不論如何,此模式仍然非常有用,但你必須小心陷阱。

舉個例子,將事件用作被動操控型命令(Passive-aggressive command)就屬於這種陷阱。它指的是源系統期待接收方執行一個動作,此時本該使用命令訊息(Command message)來展現此意圖,然而卻使用了事件。

當提到“事件驅動”時,我們在說什麼?

事件不需要包含太多資料,通常只有一些ID資訊和一個指向傳送方、可供查詢更多資訊的連結。 接收方知道它已發生變化,並且接收到關於變化的最少資訊,隨後會向傳送方發出請求,以決定下一步該做什麼。

事件攜帶的狀態轉移(Event-Carried State Transfer)

採用此模式時,可以在不需要訪問源系統的情況下,更新客戶端的資訊。客戶管理系統可能在客戶修改自己的詳細資訊(如地址)時丟擲事件,事件包含了詳細的修改資料。因此,接收方無需與客戶管理系統通訊,就可以更新自己的客戶資料副本,以進行下一步的操作。

這種模式的一個明顯缺點是,有很多冗餘資料和副本。但在儲存很便宜的時代,這不是一個問題。我們獲得了更好的彈性,因為即使客戶管理系統不可用時,接收方系統仍然可以正常工作。我們減少了延遲,因為訪問客戶資訊不需要遠端呼叫。我們也不必擔心所有來自消費端的查詢給客戶管理系統帶來的負載。但它確實給事件接收端帶來了更多複雜性,因為它必須維護所有狀態,而如果它直接訪問事件傳送方查詢資訊,通常會更加容易。

事件溯源

事件溯源(Event Sourcing)的核心思想是,每當系統狀態發生變化時,都將狀態更改記錄為事件,這樣我們就有信心在任何時間都能夠通過重新處理事件來重建系統狀態。事件庫成為事實的主要來源,系統狀態完全來源於它。對於程式設計師來說,最好的例子就是版本控制系統。所有的提交日誌就是事件庫,原始碼樹的工作副本是系統狀態。

事件溯源會引入很多問題,但我不會在這裡討論,我想強調一些常見的誤解。事件處理不必是非同步的,以更新本地Git庫為例,這完全是一個同步操作,就像更新Subversion這樣的集中式版本控制系統一樣。當然擁有所有這些提交允許你做各種有趣的事情,Git就是一個很好的例子,但核心提交從根本上說是一個簡單的動作。

另一個常見錯誤是,假定使用事件溯源系統的每個人都應該理解並訪問事件日誌以確定有用的資料,但實際上他們很可能對事件日誌只具備有限的瞭解。我正在使用編輯器寫這篇文章,編輯器不知道我的原始碼樹中的所有提交,它只是假設磁碟上有一個檔案。在基於事件溯源的系統中,很多處理可以基於一個有效的工作副本。只有當真正需要事件日誌中的資訊時才必須處理它。如果需要的話,我們可以有多個不同Schema的工作副本,但通常應該在領域處理和通過事件日誌派生工作副本之間做明確區分。

當提到“事件驅動”時,我們在說什麼?

使用事件日誌時,構建工作副本的快照通常很有用,這樣你就不必在每次需要工作副本時都從頭開始處理所有事件。實際上這裡存在二元性,我們可以將事件日誌視為變更列表或狀態列表。 我們可以從一個派生出另一個。版本控制系統通常在事件日誌中混合快照和增量變更,以獲得最佳效能。[1]

考慮一下版本控制系統帶來的價值,就很容易明白事件溯源有許多有趣的收益。事件日誌提供了強大的審計功能(賬戶交易是帳戶餘額的事件溯源)。我們可以重放事件日誌到某個點來重新建立歷史狀態。在重放時注入假設事件可以探索不一樣的歷史。事件溯源使得非持久化的工作副本(例如Memory Image)變得合理可行。

事件溯源也有自己的問題。 當結果依賴於與外部系統的互動時,重放事件就會成為問題。隨著時間的推移,我們必須清楚如何處理事件Schema的變化。許多人發現事件處理給系統增加了很多複雜性(儘管我很想知道,主要原因是不是工作副本派生元件和領域處理元件之間糟糕的隔離)。

CQRS

命令查詢職責分離(CQRS)是指讀取和寫入分別擁有單獨的資料結構。 嚴格地說,CQRS跟事件沒有關係,因為你完全不需要任何事件就可以使用CQRS。但通常人們會將CQRS與之前的模式結合起來,因此我們在峰會上就此進行了討論。

使用CQRS的理由是,在複雜領域中,使用單一模型處理讀取和寫入過於複雜,我們可以通過分離模型來簡化。當訪問模式有區別時(例如大量讀取和非常少的寫入),這一點尤其具有吸引力。但是,需要注意平衡CQRS的收益和分離模型所帶來的額外複雜度。我發現很多同事對使用CQRS非常警惕,發現它經常被濫用。

理解這些模式

作為一名熱衷於收集樣本的“軟體植物學家”,我發現這是一個棘手的地帶, 主要問題在於不同模式的混淆。 在某個專案中,一位能力很強,經驗豐富的專案經理告訴我,事件溯源是一場災難,任何變化都需要兩倍的時間來修改讀和寫模型。 在他這句話中,可以發現事件溯源和CQRS之間可能存在混淆,我們如何找出哪個是罪魁禍首? 該專案的技術主管聲稱主要問題是大量的非同步通訊,這當然是一個已知的複雜性助推器,但非同步通訊不是事件溯源或CQRS的必要組成部分。 總的來說,我們必須要注意這些模式在對的地方都很好,反之則很糟糕。 但是當我們混淆了這些模式時,很難弄清楚哪裡是對的地方。

更多精彩洞見,請關注微信公眾號:ThoughtWorks洞見

相關文章