不要只儲存網域中數據的目前狀態,而是使用僅附加存放區來記錄該數據所採取的完整系列動作。 存放區可作為記錄系統,可用來具體化領域物件。 這可以簡化複雜網域中的工作,方法是避免需要同步處理數據模型和商務領域,同時改善效能、延展性和回應性。 它也可以為事務數據提供一致性,並維護可啟用補償動作的完整稽核線索和歷程記錄。
內容和問題
大部分的應用程式都使用數據,而一般方法是讓應用程式藉由在使用者處理數據時更新數據,以維護數據的目前狀態。 例如,在傳統的建立、讀取、更新和刪除 (CRUD) 模型中,一般數據處理程式是從存放區讀取數據、對它進行一些修改,並使用新值更新數據的目前狀態,通常是使用鎖定數據的交易。
CRUD 方法有一些限制:
CRUD 系統會直接對數據存放區執行更新作業。 這些作業可能會降低效能和回應能力,而且可能會因為處理額外負荷而限制延展性。
在具有許多並行使用者的共同作業網域中,數據更新衝突的可能性更大,因為更新作業發生在單一數據項上。
除非有另一個稽核機制記錄個別記錄中每個作業的詳細數據,否則記錄會遺失。
解決方案
事件來源模式會定義處理由一連串事件所驅動之數據作業的方法,每個作業都會記錄在僅附加存放區中。 應用程式程式代碼會傳送一系列事件,以命令方式描述數據上發生的每個動作,並將其保存至事件存放區。 每個事件都代表一組數據的變更(例如 AddedItemToOrder
)。
事件會保存在事件存放區中,做為記錄系統(權威數據源)的數據目前狀態。 事件存放區通常會發佈這些事件,讓取用者可以收到通知,並在需要時加以處理。 例如,取用者可以起始將事件中作業套用至其他系統的工作,或執行完成作業所需的任何其他相關聯動作。 請注意,產生事件的應用程式程式代碼會與訂閱事件的系統分離。
事件存放區所發行事件的一般用法是維護實體的具體化檢視,因為應用程式中的動作會變更實體,以及與外部系統整合。 例如,系統可以維護用來填入UI部分之所有客戶訂單的具體化檢視。 應用程式會新增訂單、新增或移除訂單上的專案,以及新增出貨資訊。 描述這些變更的事件可以處理,並用來更新 具體化檢視。
在任何時間點,應用程式都有可能讀取事件的歷程記錄。 然後,您可以使用它來具體化實體的目前狀態,方法是播放並取用與該實體相關的所有事件。 處理要求時,此程式可能會視需要具體化網域物件。 或者,程式會透過排程的工作進行,以便實體的狀態可以儲存為具體化檢視,以支持呈現層。
此圖顯示模式的概觀,包括使用事件數據流的一些選項,例如建立具體化檢視、整合事件與外部應用程式和系統,以及重新執行事件以建立特定實體目前狀態的投影。
事件來源模式提供下列優點:
事件是不可變的,而且可以使用僅限附加的作業來儲存。 起始事件的使用者介面、工作流程或進程可以繼續,處理事件的工作可以在背景中執行。 此程式結合在處理交易期間沒有爭用的事實,可以大幅改善應用程式的效能和延展性,特別是針對簡報層級或使用者介面。
事件是描述所發生之動作的簡單物件,以及描述事件所代表動作所需的任何相關聯數據。 事件不會直接更新數據存放區。 它們只會記錄在適當的時間進行處理。 使用事件可以簡化實作和管理。
事件通常對領域專家有意義,而對象關係型不相符可能會使複雜的資料庫數據表難以理解。 數據表是代表系統目前狀態的人工建構,而不是發生的事件。
事件來源有助於防止並行更新造成衝突,因為它可避免直接更新數據存放區中的物件。 不過,領域模型仍必須設計為保護自己免受可能導致不一致狀態的要求。
僅附加事件記憶體提供稽核線索,可用來監視針對數據存放區所採取的動作。 它可以隨時重新執行事件,以具體化檢視或投影的形式重新產生目前狀態,並可協助測試和偵錯系統。 此外,使用補償事件取消變更的需求可以提供已反轉之變更的歷程記錄。 如果模型儲存目前狀態,則這項功能不會是這種情況。 事件清單也可用來分析應用程式效能,以及偵測用戶行為趨勢。 或者,它可以用來取得其他有用的商務資訊。
事件存放區會引發事件,而工作會執行作業以響應這些事件。 這會將工作與事件分離,可提供彈性和擴充性。 工作知道事件類型和事件數據,但不知道觸發事件的作業。 此外,多個工作可以處理每個事件。 這可讓您輕鬆地與其他服務與系統整合,這些服務與系統只會接聽事件存放區引發的新事件。 不過,事件來源事件通常會非常低層級,因此可能需要改為產生特定的整合事件。
事件來源通常會與 CQRS 模式結合,方法是執行數據管理工作以回應事件,以及具體化來自預存事件的檢視。
問題和考量
當您決定如何實作此模式時,請考慮下列幾點:
只有在建立具體化檢視或藉由重新執行事件來產生數據的投影時,系統才會最終保持一致。 應用程式在處理要求、正在發行的事件以及處理事件的事件取用者時,將事件新增至事件存放區之間會有一些延遲。 在此期間,描述實體進一步變更的新事件可能已抵達事件存放區。 系統應設計成在這些案例中考慮最終一致性。
事件存放區是資訊的永久來源,因此不應該更新事件數據。 更新實體以復原變更的唯一方法是將補償事件新增至事件存放區。 如果保存事件的格式(而非數據)需要變更,或許在移轉期間,很難將存放區中的現有事件與新版本結合。 可能需要逐一查看所有進行變更的事件,使其符合新格式,或新增使用新格式的事件。 請考慮在每個事件架構版本上使用版本戳記,以維護舊事件和新事件格式。
多線程應用程式和多個應用程式實例可能會將事件儲存在事件存放區中。 事件存放區中事件的一致性非常重要,因為影響特定實體的事件順序(實體變更的順序會影響其目前狀態)。 將時間戳新增至每個事件有助於避免問題。 另一個常見的作法是標註要求所產生的每個事件,並加上累加標識符。 如果兩個動作嘗試同時新增相同實體的事件,事件存放區可以拒絕符合現有實體標識碼和事件標識碼的事件。
沒有標準方法,或現有的機制,例如 SQL 查詢,用於讀取事件以取得資訊。 唯一可以擷取的數據是使用事件標識碼作為準則的事件數據流。 事件標識碼通常會對應至個別實體。 只有重新執行與實體原始狀態相關的所有事件,才能判斷實體的目前狀態。
每個事件數據流的長度會影響管理和更新系統。 如果數據流很大,請考慮在特定間隔建立快照集,例如指定的事件數目。 實體的目前狀態可以從快照集取得,並重新執行在該時間點之後發生的任何事件。 如需建立數據快照集的詳細資訊,請參閱 主要-從屬快照集復寫。
雖然事件來源可將數據更新衝突的機會降到最低,但應用程式仍必須能夠處理最終一致性和缺乏交易所造成的不一致。 例如,表示庫存庫存減少的事件可能會在放置該專案的訂單時抵達數據存放區。 這種情況會導致要求協調這兩項作業,方法是建議客戶或建立回訂單。
事件發行集可能至少是 一次,因此事件的取用者必須是等冪。 如果事件處理一次以上,則它們不得重新套用事件中所述的更新。 取用者的多個實例可以維護和匯總實體的屬性,例如訂單總數。 當順序放置事件發生時,只有一個必須成功遞增匯總。 雖然此結果不是事件來源的重要特性,但它是通常的實作決策。
選取的事件記憶體必須支援應用程式所產生的事件負載。
請注意一個事件的處理涉及建立一或多個新事件的案例,因為這可能會導致無限迴圈。
使用此模式的時機
在下列案例中使用此模式:
當您想要擷取數據的意圖、用途或原因時。 例如,客戶實體的變更可以擷取為一系列特定的事件類型,例如 已移動回家、 已關閉的帳戶或 已死亡。
當最小化或完全避免發生數據衝突更新時,這一點非常重要。
當您想要記錄發生的事件時,請重新執行以還原系統的狀態、復原變更,或保留記錄和稽核記錄。 例如,當工作牽涉到多個步驟時,您可能需要執行動作來還原更新,然後重新執行一些步驟,讓數據回到一致的狀態。
當您使用事件時。 這是應用程式作業的自然功能,需要很少額外的開發或實作工作。
當您需要分離輸入的程式,或從套用這些動作所需的工作更新數據時。 這項變更可能是為了改善UI效能,或將事件散發給其他在事件發生時採取動作的接聽程式。 例如,您可以將薪資系統與費用提交網站整合。 事件存放區為了響應網站中所做的數據更新而引發的事件,將會由網站和薪資系統取用。
如果您想要彈性在需求變更時變更具體化模型和實體數據的格式,或搭配 CQRS 使用時,您需要調整讀取模型或公開數據的檢視。
與 CQRS 搭配使用時,在更新讀取模型時可接受最終一致性,或可接受從事件數據流重新凍結實體和數據效能的影響。
此模式可能無法用於下列情況:
小型或簡單的網域、幾乎沒有商業規則的系統,或自然適用於傳統 CRUD 數據管理機制的非網域系統。
需要對數據檢視進行一致性和即時更新的系統。
不需要復原和重新執行動作的稽核線索、歷程記錄和功能系統。
只有低度發生基礎數據衝突更新的系統。 例如,主要新增數據而不是更新數據的系統。
工作負載設計
架構設計人員應該評估事件來源模式如何用於其工作負載的設計,以解決 Azure 架構架構支柱中涵蓋的目標和原則。 例如:
要素 | 此模式如何支援支柱目標 |
---|---|
可靠性設計決策可協助工作負載復原到故障,並確保它會在發生失敗后復原到完全正常運作的狀態。 | 由於擷取複雜商務程式中變更的歷程記錄,因此如果您需要復原狀態存放區,它可以促進狀態重建。 - RE:06 數據分割 - RE:09 災害復原 |
效能效率可透過調整規模、資料、程式碼達到最佳化,有效率地協助您的工作負載符合需求。 | 此模式通常結合 CQRS、適當的定義域設計和策略快照集,可改善工作負載效能,因為不可部分完成的附加作業,以及避免資料庫鎖定進行寫入和讀取。 - PE:08 數據效能 |
如同任何設計決策,請考慮對其他可能以此模式導入之目標的任何取捨。
範例
會議管理系統必須追蹤會議已完成的預約次數。 如此一來,當潛在出席者嘗試預約時,它可以檢查是否有座位可供使用。 系統會以至少兩種方式儲存會議預約總數:
系統會將預訂總數的相關信息儲存為保存預約資訊的資料庫中個別實體。 由於預約已進行或取消,系統可能會視需要遞增或遞減此號碼。 從理論上講,這種方法很簡單,但如果大量的出席者在短時間內嘗試預訂座位,可能會導致延展性問題。 例如,在預約期間關閉前的最後一天左右。
系統會將預約和取消的相關信息儲存為活動存放區中保留的事件。 然後,您可以重新執行這些事件來計算可用的基座數目。 由於事件不變性,這種方法可以更擴充。 系統只需要能夠從事件存放區讀取數據,或將數據附加至事件存放區。 預訂和取消的相關事件信息永遠不會修改。
下圖說明如何使用事件來源實作會議管理系統的座位保留子系統。
保留兩個基座的動作順序如下:
使用者介面會發出命令,為兩位出席者保留基座。 此命令是由個別的命令處理程序處理。 與使用者介面分離的邏輯片段,負責處理張貼為命令的要求。
包含會議所有保留相關信息的匯總,是藉由查詢描述預約和取消的事件來建構。 這個匯總稱為
SeatAvailability
,並包含在定義域模型中,其會公開查詢和修改匯總中數據的方法。要考慮的一些優化是使用快照集(因此您不需要查詢和重新執行事件的完整清單,以取得匯總的目前狀態),以及在記憶體中維護匯總的快取複本。
命令處理程式會叫用領域模型公開的方法,以建立保留。
匯總
SeatAvailability
會記錄事件,其中包含保留的基座數目。 下次匯總套用事件時,所有保留都會用來計算保留的基座數目。系統會將新事件附加至事件存放區中的事件清單。
如果使用者取消座位,系統會遵循類似的程式,但命令處理程式會發出產生基座取消事件的命令,並將它附加至事件存放區。
除了提供更多延展性範圍之外,使用事件存放區也提供會議預約和取消的完整歷程記錄或稽核記錄。 事件存放區中的事件是精確的記錄。 不需要以任何其他方式保存匯總,因為系統可以輕鬆地重新執行事件,並將狀態還原至任何時間點。
您可以在事件來源簡介中找到此範例的詳細資訊。
下一步
數據一致性入門。 當您搭配個別的讀取存放區或具體化檢視使用事件來源時,讀取數據不會立即保持一致。 相反地,數據只會最終保持一致。 本文摘要說明維護分散式數據一致性的相關問題。
數據分割指引。 當您使用事件來源來改善延展性、減少爭用並優化效能時,數據通常會分割。 本文說明如何將數據分割成離散分割區,以及可能發生的問題。
馬丁·福勒的部落格:
相關資源
實作此模式時,下列模式和指引也可能相關:
命令和查詢責任隔離 (CQRS) 模式。 提供 CQRS 實作之永久資訊來源的寫入存放區,通常是以事件來源模式的實作為基礎。 描述如何隔離讀取應用程式中數據的作業,以及使用個別介面來更新數據的作業。
具體化檢視模式。 以事件來源為基礎的系統中所使用的數據存放區通常不適合有效率的查詢。 相反地,常見的方法是定期或數據變更時產生預先填入的數據檢視。
補償交易模式。 事件來源存放區中的現有數據不會更新。 相反地,會加入新的專案,以將實體的狀態轉換為新值。 若要反轉變更,會使用補償專案,因為無法反轉先前的變更。 描述如何復原先前作業所執行的工作。