從 CRUD 遷移到事件溯源的秘訣 - eventstore
VSole2022-08-08 10:12:07
事件溯源是高性能協作域的一種很好的架構風格,可以保證它增加的復雜性。但正如我之前所說,就像任何其他原則或實踐一樣,即使是事件溯源也有利有弊。而且它不是頂級架構。您系統的某些部分可能會從中受益,但其他部分可能不會。話雖如此,如果您需要事件溯源,并且您有一個現有的、更傳統的(又名 CRUD)應用程序,您可以遵循大致三種策略:
- 保持一切原樣,僅使用事件溯源構建系統的新部分
- 通過并排重建現有子系統或域來隱藏它。
- 然后,在重建完成后,切換所有現有消費者并自動遷移數據。
- 對現有域進行逐個實體的逐步遷移
大約七年前,我們逐漸將使用命令查詢職責分離 (CQRS) 模式設計的現有 .NET 應用程序轉換為事件溯源。由于前兩個場景已經寫了很多,讓我分享我們為后者采取的秘訣。
讓我們從建立術語開始。在更傳統的系統中,您的域由實體組成。在事件溯源世界中,您經常會看到幾個相關實體形成了一個事務邊界。在領域驅動設計中,這稱為聚合。大多數事件存儲使用術語流來捕獲該聚合中曾經發生的所有事件。并且該聚合中通常有一個實體作為唯一的入口點。這是聚合根,由唯一編號或鍵(流 ID )標識。現在我們已經解決了這個問題,這里有一些實用的步驟來幫助你前進。
- 弄清楚您當前的域是否依賴于跨多個實體的事務,以及事件存儲實現是否支持跨聚合(或跨流)事務。
- 仔細決定哪些實體將形成聚合。
- 如果您的聚合太大,并且您還沒有準備好采用事件合并技術,則會增加用戶運行在樂觀并發問題中的機會。
- 如果您的聚合太小,并且您的事件存儲不支持跨聚合事務,則您必須以功能方式處理這些業務規則,例如,使用補償操作。
- 這就是為什么讓這些不變量幫助您定義聚合的邊界如此重要。
- 確定哪個實體應作為聚合根、聚合的入口點,并向其添加版本。
- 確保對聚合內實體的任何更改都會影響版本。
- 如果那里已經有一個版本,我們建議通過將事件數添加到原始版本號來計算新版本。
- 確保沒有其他代碼可以在不首先通過聚合根的情況下改變聚合內實體的狀態。
- 將子實體上的可寫屬性和公共方法替換為根上的方法,因此根控制訪問,可以保護業務規則,生成唯一的子 ID 并提高版本。
- 刪除跨聚合的實體之間的直接依賴關系。
- 例如,在對象關系映射器支持的許多域中,具有延遲加載屬性是很常見的。
- 您需要重構任何依賴于它的代碼,或者引入和注入存儲庫抽象。
- 確保實體不知道持久性并且不直接訪問數據庫。
- 要么將其移動到處理來自您的 API 的傳入請求的命令處理程序,要么為此引入存儲庫抽象。
- 為該聚合確定一個自然分區鍵,這樣您就可以在事件存儲變得非常大并導致性能問題或存儲問題的情況下拆分事件。
- 一個很好的分區鍵是以這樣一種方式分離數據的東西,您不需要跨分區處理業務規則。
- 例如,您的域可能是按地理區域或公司組織的。
- 在多租戶域中,租戶 ID 將是一個很好的候選者。
- 由于您不應修改歷史記錄,因此事件溯源中的刪除概念略有不同。
- 盡管您在技術上可以從底層事件存儲中刪除事件,但您通常會采用更實用的方法并使用事件將聚合標記為已刪除。
- 因此,任何用于請求實體的特定實例并準備好找不到任何內容的查詢都必須明確采用或通過某種抽象采用。
- 一個常見的解決方案是將 IsDeleted 屬性添加到存儲庫實現可以檢查的聚合根。
- 考慮數據導入需求。
- 如果您習慣于直接通過表導入數據,則必須將其更改為 CLI 或 HTTP API 之類的內容。
- 還要決定是要通過現有的“屬性更改”事件還是通過專門的“數據已導入”事件來處理該導入。
- 仔細確定如何將實體的原始鍵映射到流 ID。
- 大多數事件存儲支持使用字符串作為流 ID,但如果不經過一些更復雜的循環,就不可能在事后更改 ID。
- 如果您的商店僅使用 GUID,您可以使用像這樣的確定性 Guid 生成器。
- 并且不要忘記內部密鑰與您在域外公開的密鑰之間存在差異。
- 與此密切相關的是,在事件溯源中保證唯一性的工作方式略有不同。
- 因此,如果您的域依賴于數據庫模式來保護唯一約束,您將需要找到替代方案(例如使用流 ID)。
- 引入用于從/向事件存儲加載和保存聚合的基礎結構,并從持久化事件中重新混合聚合。
- 您可以在此處、此處和此處找到一些有關如何執行此操作的示例以及 .NET 中聚合根的基類。
- 到目前為止,我們主要使用這些參考作為示例,而不是作為框架來構建我們的域。
- 如果您有存儲庫抽象,請確保它知道哪些實體已轉換并需要從事件存儲中加載,哪些仍需要從原始表中加載。
- 為此,我們使用了標記接口或 .NET 屬性。
- 推遲諸如快照之類的決定,直到您需要它們為止。
- 對于最終具有大量事件的聚合來說,快照是一種有效的解決方案。
- 但是,在您獲得足夠的性能結果來保證這種復雜性之前,不要去那里。
- 決定如何將存儲在數據庫中的現有實體轉換為事件源聚合。
- 過去,我們試圖將現有記錄映射到單個的、更多“屬性更改”的事件中。
- 回想起來,我們應該已經定義了一次性轉換事件。
- 確定您是否希望使投影代碼在事務上與聚合發出的事件一致,以及這是否會給您可接受的性能。
- 如果您不這樣做,并且所有投影表都是異步構建的,請確保代碼庫的其余部分不希望投影表上的查詢保持一致。
- 設計將現有數據轉換為新的事件源模型的策略。
- 例如,這就是我們所做的:
- [list=1]
- 使用臨時名稱重命名現有表及其子表
- 一一讀取記錄并使用您在前面步驟中設計的事件構建新的聚合
- 將這些新事件投影到一組新的表中,這些表的名稱和結構與遷移開始前的樣子相同
- 轉換和投影后立即從臨時表中刪除每條記錄
- 刪除臨時表
- 對其余實體重復前面的步驟,但不要猶豫,在生產中發布中間步驟。
- 根據您的需要構建更優化的投影。
- 但不要忘記,第一個目標是轉換您現有的代碼庫。
本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
VSole
網絡安全專家