「事件驅動」是什麼意思?
2017 年 2 月 7 日
去年年底,我參加了 Thoughtworks 同事舉辦的研討會,討論「事件驅動」應用程式的本質。過去幾年來,我們建構了許多廣泛使用事件的系統,它們經常受到讚譽,但也經常受到批評。我們的北美辦公室組織了一場高峰會,來自世界各地的 Thoughtworks 高級開發人員齊聚一堂,分享想法。
高峰會最大的成果是認知到,當人們談論「事件」時,他們實際上指的是一些截然不同的事情。因此,我們花了很多時間試圖找出一些有用的模式。這份筆記簡要總結了我們識別出的主要模式。
事件通知
當系統發送事件訊息以通知其他系統其網域中變更時,就會發生這種情況。事件通知的一個關鍵要素是,來源系統並不太在意回應。通常它根本不期待任何答案,或者如果來源確實在意回應,那也是間接的。發送事件的邏輯流程與回應該事件某種反應的任何邏輯流程之間會有明顯的分離。
事件通知很不錯,因為它表示低耦合,而且設定起來相當簡單。但是,如果真的有邏輯流程執行在各種事件通知上,它可能會產生問題。問題在於,由於它在任何程式碼文字中都不是明確的,因此很難看到這樣的流程。通常,找出這個流程的唯一方法是監控即時系統。這可能會讓除錯和修改這樣的流程變得困難。風險在於,使用事件通知建立一個良好的解耦系統非常容易,卻沒有意識到會失去對較大規模流程的掌握,因此在未來幾年為自己製造麻煩。這個模式仍然非常有用,但你必須小心這個陷阱。
這個陷阱的一個簡單範例是,當一個事件被用作被動攻擊命令時。當來源系統預期接收者執行一個動作,而且應該使用命令訊息來顯示那個意圖,但卻將訊息樣式設定為事件時,就會發生這種情況。
事件不需要攜帶太多資料,通常只有一些 ID 資訊和一個連結回可以查詢更多資訊的寄件者。接收者知道有些東西已經改變,可能會取得一些關於變更性質的最小資訊,但接著會發出請求回到寄件者,以決定接下來要做什麼。
事件傳遞狀態轉移
當你想要更新系統的客戶端,讓他們不需要聯絡來源系統就可以做進一步的工作時,就會出現這個模式。客戶管理系統可能會在客戶變更其詳細資料(例如地址)時發出事件,其中包含已變更資料的詳細資料。然後,接收者可以使用變更來更新其自己的客戶資料副本,這樣它就不需要在未來與主要客戶系統交談以執行其工作。
這個模式一個明顯的缺點是,有大量的資料被拖來拖去,而且有很多副本。但在儲存空間充裕的時代,這就不是什麼問題了。我們獲得的是更大的復原力,因為當客戶系統變得不可用時,接收者系統可以運作。我們減少了延遲,因為不需要遠端呼叫來存取客戶資訊。我們不必擔心客戶系統的負載,以滿足所有消費者系統的查詢。但它確實讓接收者變得更複雜,因為它必須整理出維護所有狀態的方法,而當需要時,通常只要呼叫寄件者取得更多資訊就比較容易。
事件溯源
事件溯源的核心概念是,每當我們對系統的狀態進行變更時,我們會將那個狀態變更記錄為一個事件,而且我們可以自信地透過在未來任何時間重新處理事件來重建系統狀態。事件儲存庫成為真實的主要來源,而系統狀態純粹從中衍生。對於程式設計師來說,最好的範例就是版本控制系統。所有提交的記錄就是事件儲存庫,而原始樹的工作副本就是系統狀態。
事件來源會產生許多問題,我不會在此深入探討,但我確實想強調一些常見的誤解。事件處理不需要非同步,考慮更新本機 git 儲存庫的情況,這完全是同步操作,就像更新 Subversion 等集中式版本控制系統一樣。當然,擁有所有這些提交記錄可讓您執行各種有趣的行為,git 就是一個很好的例子,但核心提交基本上是一個簡單的動作。
另一個常見的錯誤是假設使用事件來源系統的每個人都應該了解並存取事件記錄,以確定有用的資料。但事件記錄的知識可能有限。我正在編輯器中撰寫這篇文章,它忽略了原始碼樹中的所有提交記錄,它只假設磁碟上有檔案。事件來源系統中的大部分處理可以基於有用的工作副本。只有真正需要事件記錄中資訊的元素才應該操作它。如果這有幫助,我們可以有多個具有不同架構的工作副本;但通常應該明確區分網域處理和從事件記錄衍生工作副本。
使用事件記錄時,建立工作副本的快照通常很有用,這樣您就不必每次需要工作副本時都從頭處理所有事件。的確這裡存在二元性,我們可以將事件記錄視為變更清單或狀態清單。我們可以從其他衍生一個。版本控制系統通常會在事件記錄中混合快照和增量,以獲得最佳效能。[1]
事件來源有許多有趣的優點,在思考版本控制系統的價值時很容易想到這些優點。事件記錄提供強大的稽核功能(會計交易是帳戶餘額的事件來源)。我們可以透過將事件記錄重新播放到某個時間點來重新建立歷史狀態。我們可以在重新播放時注入假設事件來探索替代歷史。事件來源使得擁有非耐用的工作副本(例如記憶體映像)成為可能。
事件來源確實有其問題。當結果取決於與外部系統的互動時,重新播放事件會變得有問題。我們必須找出如何處理事件架構隨著時間推移而產生的變更。許多人發現事件處理為應用程式增加了許多複雜性(儘管我確實想知道這是否更多是基於衍生工作副本的元件和執行網域處理的元件之間的區分不佳)。
CQRS
命令查詢責任隔離(CQRS)是針對讀取和寫入資訊擁有獨立資料結構的概念。嚴格來說,CQRS 並非真正與事件有關,因為你可以在設計中使用 CQRS,而不需要任何事件。但一般來說,人們會將 CQRS 與此處較早的模式結合,因此它們會出現在高峰會上。
CQRS 的理由在於,在複雜的領域中,單一模型用於處理讀取和寫入會變得過於複雜,而我們可以透過區分模型來簡化。當你擁有不同的存取模式時,這特別有吸引力,例如大量的讀取和極少的寫入。但使用 CQRS 的好處必須與擁有獨立模型的額外複雜性取得平衡。我發現許多同事都非常謹慎地使用 CQRS,並發現它經常被誤用。
了解這些模式
作為一種熱衷於收集範例的軟體植物學家,我發現這是一個棘手的領域。核心問題是混淆不同的模式。在一個專案中,有能力且經驗豐富的專案經理告訴我,事件來源是一場災難 - 任何變更都需要兩倍的工作才能更新讀取和寫入模型。就在那句話中,我就能發現事件來源和 CQRS 之間潛在的混淆 - 那麼,我如何找出罪魁禍首?該專案的技術負責人聲稱,主要問題是大量的非同步通訊,這當然是一個已知的複雜性提升因素,但並非事件來源或 CQRS 的必要部分。此外,我們必須注意,所有這些模式在適當的地方都是好的,但在錯誤的領域中使用時卻很糟糕。但是,當我們合併模式時,很難找出什麼是正確的領域。
我很想寫一些明確的論文來解決所有這些混淆,並提供有關如何做好每種模式以及何時應該使用的具體指南。遺憾的是,我沒有時間去做。我寫這份說明,希望它能派上用場,但很清楚地知道它遠遠達不到實際需要。
進一步閱讀
我準備了一個關於此主題的演講,這是 2017 年芝加哥 goto 的主題演講。
早在 2006 年,我寫了一堆原型模式,並考慮製作我的 P of EAA 書籍 的另一冊。遺憾的是,即使過了十年,我仍然沒有時間繼續這項工作。不過,我當時寫的東西就在那裡可以閱讀。對於事件,我會從 專注於事件 開始,這總結了我當時對使用事件的想法。雖然已經有一段時間了,但我認為我當時寫的大部分內容仍然有效。
這些文章中最具影響力的,是關於 事件溯源 的文章。它主要討論使用重播來形成歷史和替代狀態的價值。
關於 事件協作 的文章觸及事件通知和事件承載狀態轉移的模式,但混淆了這些模式(僅在研討會期間,我才開始將它們視為獨立的模式)。
我在 CQRS 上有一篇 bliki 文章。
網路上還有更多關於這些主題的資料,所以盡情探索吧。我沒有任何評論,因為我沒有花時間瀏覽並挑選一些建議。
註腳
1: 我有時聽到人們說 git 不是事件溯源的範例,因為它將檔案和樹的狀態儲存在 .git/objects
中。但是系統是否使用變更或快照作為其內部儲存,並不會影響它是否為事件溯源。git 很樂意根據需要為我彙整變更清單。當它將資料壓縮成 packfile 時,它確實會使用快照和變更的組合,並根據效能原因選擇組合。
更新
2017-02-08:微調對事件記錄與應用程式狀態使用的討論,並新增註腳以釐清 git 和快照的角色。