概念解釋
事件溯源(Event Sourcing)是一種設計模式,其核心思想是將系統的狀態變化表示為一系列不可變的事件,并將這些事件存儲在事件日志中。系統的當前狀態可以通過重新應用(回放)這些事件來還原,從而實現狀態的追溯。
傳統方式
大多數應用程序會使用數據,而典型的方法是用戶使用數據時通過立即更新數據使應用程序保持數據的當前狀態。 例如,在傳統的創建、讀取、更新和刪除 (CRUD) 模型中,典型的數據處理是從存儲讀取數據、對其作出修改、使用新值更新數據的當前狀態。
CRUD 方法具有一些限制:
- CRUD 系統直接對數據存儲執行更新操作。 這些操作所需的處理工作開銷可能會降低性能和響應能力,并會限制可擴展性。
- 在包含多個并發用戶的協作域中,由于會對數據單個項進行更新操作,因此出現數據更新沖突的可能性更大。
- 除非有其他審核機制可以單獨記錄每個操作的詳細信息,否則歷史記錄會丟失。
事件溯源模式優點
- 事件不可變,并且可使用只追加操作進行存儲。 用戶界面、工作流或啟動事件的進程可繼續,處理事件的任務可在后臺運行。
- ?事件不會直接更新數據存儲。 只會對事件進行記錄,以便在合適的時間進行處理。 使用事件可簡化實現和管理
- 事件溯源不需要直接更新數據存儲中的對象,因而有助于防止并發更新造成沖突。 但是,域模型必須仍然設計為避免可能導致不一致狀態的請求。
- 事件的只追加存儲提供的審核線索可用于監視對數據存儲采取的操作。 它可以通過隨時重播事件將當前狀態重新生成為具體化視圖或投影,并且可以幫助測試和調試系統,事件列表還可用于分析應用程序性能和檢測用戶行為趨勢。
事件溯源模式缺點
- 只有通過重播事件創建具體化視圖或生成數據投影時,系統才可實現最終一致性。 應用程序將事件添加到事件存儲作為處理請求的結果、發布事件和事件使用者處理事件之間存在一定程度的延遲。 在此期間,描述實體的進一步更改的新事件可能已到達事件存儲。 系統設計應考慮到這些方案中實現最終一致性
問題和注意事項
事件存儲是信息的永久源,因此請勿更新事件數據。 更新實體以撤銷更改的唯一方式是將補償事件添加到事件存儲。 如果持久化事件的格式(而不是數據)需要更改,也許在遷移期間,很難將存儲中的現有事件和新版本結合。 可能需要循環訪問所有事件進行更改,使其符合新格式,或添加使用新格式的新事件。 考慮在事件架構的每個版本上使用版本標記,以同時保留事件的舊格式和新格式。
多線程應用程序和應用程序的多個實例可能將事件存儲在事件存儲中。 事件存儲中的事件一致性至關重要,影響特定實體的事件的順序(實體更改發生的順序會影響當前狀態)同樣至關重要。 將時間戳添加到每個事件有助于避免出現問題。 另一常見做法是使用增量標識符注釋請求引起的每個事件。 如果兩個操作嘗試同時為同一實體添加事件,則事件存儲可拒絕與現有實體標識符和事件標識符相匹配的事件。
讀取事件以獲取信息并沒有標準方法或現有機制,例如 SQL 查詢。 可提取的唯一數據是將事件標識符用作條件的事件流。 事件 ID 通常會映射到各個實體。 僅可根據實體原始狀態通過重播與其關聯的所有事件來確定實體的當前狀態。
每個事件流的長度會影響管理和更新系統。 如果是大型流,請考慮按特定間隔(例如指定數量的事件)創建快照。 可通過快照和重播此時間點后發生的事件獲取實體的當前狀態。
即使事件溯源會最大程度降低數據更新沖突的可能性,應用程序仍必須能夠處理由最終一致性和缺少事務引起的不一致性。 例如,在指示存貨減少的事件到達數據存儲時,客戶可能正在對該商品下訂單。 這種情況導致需要在這兩個操作之間作出協調,即通知客戶或創建延期交付訂單。
事件發布可能是“至少一次”,因此事件使用者必須是冪等的。 如果事件處理次數大于 1,則使用者不得重新應用該事件中描述的更新。 使用者 cn 的多個實例維護并聚合實體的屬性,例如已下訂單總數。 下訂單事件發生時,只有一個實例必須成功遞增聚合。 盡管這個結果不是事件溯源的主要特點,但卻是通常的實現決策。
事件在事件存儲中持久化,事件存儲充當數據當前狀態的記錄系統(權威數據源)。 事件存儲通常會發布這些事件,訂閱者可收到通知并在需要時對其進行處理。請注意,生成事件的應用程序代碼應與訂閱到事件的系統分離。
所選的事件存儲需要支持由應用程序生成的事件負載。
請注意以下情況:處理一個事件會涉及創建一個或多個新事件,因為這可能會導致無限循環。
何時使用此模式
請在以下方案中使用此模式:
-
要捕獲數據中的意圖、用途或原因。 例如,可將對客戶實體的更改捕獲為一系列特定事件類型,例如“已搬家”、“帳戶已關閉”或“已身故”。
-
盡量減少或完全避免出現數據更新沖突。
-
需要記錄發生的事件,并重播事件以還原系統狀態、回滾更改或保留歷史記錄和審核日志。 例如,任務涉及多個步驟時,可能需要執行操作來恢復更新,并重播某些步驟使數據重返一致的狀態。
-
使用事件時。 這是應用程序操作的自然功能,且幾乎不需要其他開發或實現工作。
-
需要將輸入或更新數據的過程從應用這些操作所需的任務中分離。 此更改可能是為了提高 UI 的性能,或者是為了將事件分發給其他在事件發生時采取操作的偵聽器。 例如,可以將工資管理系統與開支報銷網站集成。 由事件存儲引發的用于響應網站中數據更新的事件可同時供該網站和工資管理系統使用。
-
希望隨要求更改而靈活更改具體化模型和實體數據的格式,或需要調整讀取模型或公開數據的視圖(與 CQRS 結合使用時)。
-
與 CQRS 結合使用且更新讀取模型時最終一致性可接受或事件流中的解凍實體和數據的性能影響可接受。
此模式在以下情況中可能不起作用:
-
小型域或簡單域、幾乎或完全沒有業務邏輯的系統或者自然地適用于傳統 CRUD 數據管理機制的非域系統。
-
要求一致性和數據視圖實時更新的系統。
-
不需要審核線索、歷史記錄以及回滾和重播操作功能的系統。
-
基礎數據更新沖突發生率低的系統。 例如,主要是添加數據而不是更新數據的系統。
示例
會議管理系統需要跟蹤會議的已完成預訂數。 這種方式可以檢查潛在與會者預訂時是否有可用席位。 此系統可通過至少兩種方式存儲會議的預訂總數:
-
此系統可將預訂總數信息作為單獨的實體存儲在包含預訂信息的數據庫中。 進行預訂或取消預訂時,此系統可相應地增加或減少此數量。 理論上而言,此方式很簡單,但如果短時間內有大量與會者嘗試預訂席位,則可能導致可伸縮性問題。 例如,在預訂期結束前的最后一天左右。
-
此系統可將預訂和取消預訂信息存儲為事件存儲中的事件。 可通過重播這些事件來計算可用的席位數。 由于事件的不變性,此方式更具伸縮性。 此系統僅需要可從事件存儲讀取數據,或將數據追加到事件存儲。 不會修改有關預訂和取消預訂的事件信息。
下圖說明了如何使用事件溯源實施會議管理系統的席位預訂子系統。
預訂兩個席位的操作順序如下:
-
用戶界面發出為兩位與會者預訂席位的命令。 該命令由單獨的命令處理程序處理。 一條邏輯,此邏輯從用戶界面分離且負責處理發布為命令的請求。
-
通過查詢描述預訂和取消預訂的事件,構造包含有關會議的所有預訂的信息的一個聚合。 此聚合名為?
SeatAvailability
,且包含在公開此聚合中數據的查詢和修改方法的域模型中。需要考慮的一些優化是使用快照(使獲取聚合的當前狀態無需查詢和重播事件的完整列表)和將此聚合的緩存副本保留在內存中。
-
命令處理程序調用域模型公開的方法來進行預訂。
-
SeatAvailability
?聚合會記錄包含已預訂席位數的事件。 聚合下次應用事件時,會使用所有的預訂數來計算剩余的席位數。 -
此系統將新事件追加到事件存儲中的事件列表。
如果某位用戶取消席位,此系統將執行相似過程,但命令處理程序會發出生成席位取消事件并將其追加到事件存儲的命令。
除了擴大可伸縮性范圍外,使用事件存儲還可提供會議預訂和取消預訂的完整歷史記錄或審核線索。 事件存儲中的事件是準確的記錄。 無需以其他任何方式持久化聚合,因為此系統可輕松重播事件并將狀態還原到任意時間點。
結論
通常配合CQRS模式結合使用,適用于大型應用系統,帶來便利的同時也需要考慮更多潛在且復雜的問題,如事件與實體之間的最終一致性、延遲等問題。一般小項目還是算了。不過是個很優秀的設計理念,有借鑒價值。