從 CRUD 遷移到事件溯源的祕訣 - eventstore

發表於2021-06-06

事件溯源是高效能協作域的一種很好的架構風格,可以保證它增加的複雜性。但正如我之前所說,就像任何其他原則或實踐一樣,即使是事件溯源也有利有弊。而且它不是頂級架構。您系統的某些部分可能會從中受益,但其他部分可能不會。話雖如此,如果您需要事件溯源,並且您有一個現有的、更傳統的(又名 CRUD)應用程式,您可以遵循大致三種策略:

  1. 保持一切原樣,僅使用事件溯源構建系統的新部分
  2. 通過並排重建現有子系統或域來隱藏它。然後,在重建完成後,切換所有現有消費者並自動遷移資料。
  3. 對現有域進行逐個實體的逐步遷移

大約七年前,我們逐漸將使用命令查詢職責分離 (CQRS) 模式設計的現有 .NET 應用程式轉換為事件溯源。由於前兩個場景已經寫了很多,讓我分享我們為後者採取的祕訣。 

 

讓我們從建立術語開始。在更傳統的系統中,您的域由實體組成。在事件溯源世界中,您經常會看到幾個相關實體形成了一個事務邊界。在領域驅動設計中,這稱為聚合。大多數事件儲存使用術語流來捕獲該聚合中曾經發生的所有事件。並且該聚合中通常有一個實體作為唯一的入口點。這是聚合根,由唯一編號或鍵(流 ID )標識。現在我們已經解決了這個問題,這裡有一些實用的步驟來幫助你前進。

  1. 弄清楚您當前的域是否依賴於跨多個實體的事務,以及事件儲存實現是否支援跨聚合(或跨流)事務。
  2. 仔細決定哪些實體將形成聚合。如果您的聚合太大,並且您還沒有準備好採用事件合併技術,則會增加使用者執行在樂觀併發問題中的機會。如果您的聚合太小,並且您的事件儲存不支援跨聚合事務,則您必須以功能方式處理這些業務規則,例如,使用補償操作。這就是為什麼讓這些不變數幫助您定義聚合的邊界如此重要。
  3. 確定哪個實體應作為聚合根、聚合的入口點,並向其新增版本。確保對聚合內實體的任何更改都會影響版本。如果那裡已經有一個版本,我們建議通過將事件數新增到原始版本號來計算新版本。
  4. 確保沒有其他程式碼可以在不首先通過聚合根的情況下改變聚合內實體的狀態。將子實體上的可寫屬性和公共方法替換為根上的方法,因此根控制訪問,可以保護業務規則,生成唯一的子 ID 並提高版本。
  5. 刪除跨聚合的實體之間的直接依賴關係。例如,在物件關係對映器支援的許多域中,具有延遲載入屬性是很常見的。您需要重構任何依賴於它的程式碼,或者引入和注入儲存庫抽象。
  6. 確保實體不知道永續性並且不直接訪問資料庫。要麼將其移動到處理來自您的 API 的傳入請求的命令處理程式,要麼為此引入儲存庫抽象。
  7. 為該聚合確定一個自然分割槽鍵,這樣您就可以在事件儲存變得非常大並導致效能問題或儲存問題的情況下拆分事件。一個很好的分割槽鍵是以這樣一種方式分離資料的東西,您不需要跨分割槽處理業務規則。例如,您的域可能是按地理區域或公司組織的。在多租戶域中,租戶 ID 將是一個很好的候選者。
  8. 由於您不應修改歷史記錄,因此事件溯源中的刪除概念略有不同。儘管您在技術上可以從底層事件儲存中刪除事件,但您通常會採用更實用的方法並使用事件將聚合標記為已刪除。因此,任何用於請求實體的特定例項並準備好找不到任何內容的查詢都必須明確採用或通過某種抽象採用。一個常見的解決方案是將 IsDeleted 屬性新增到儲存庫實現可以檢查的聚合根。
  9. 考慮資料匯入需求。如果您習慣於直接通過表匯入資料,則必須將其更改為 CLI 或 HTTP API 之類的內容。還要決定是要通過現有的“屬性更改”事件還是通過專門的“資料已匯入”事件來處理該匯入。
  10. 仔細確定如何將實體的原始鍵對映到流 ID。大多數事件儲存支援使用字串作為流 ID,但如果不經過一些更復雜的迴圈,就不可能在事後更改 ID。如果您的商店僅使用 GUID,您可以使用像這樣的確定性 Guid 生成器。並且不要忘記內部金鑰與您在域外公開的金鑰之間存在差異。
  11. 與此密切相關的是,在事件溯源中保證唯一性的工作方式略有不同。因此,如果您的域依賴於資料庫模式來保護唯一約束,您將需要找到替代方案(例如使用流 ID)。
  12. 引入用於從/向事件儲存載入和儲存聚合的基礎結構,並從持久化事件中重新混合聚合。您可以在此處此處此處找到一些有關如何執行此操作的示例以及 .NET 中聚合根的基類。到目前為止,我們主要使用這些參考作為示例,而不是作為框架來構建我們的域。
  13. 如果您有儲存庫抽象,請確保它知道哪些實體已轉換並需要從事件儲存中載入,哪些仍需要從原始表中載入。為此,我們使用了標記介面或 .NET 屬性。
  14. 推遲諸如快照之類的決定,直到您需要它們為止。對於最終具有大量事件的聚合來說,快照是一種有效的解決方案。但是,在您獲得足夠的效能結果來保證這種複雜性之前,不要去那裡。
  15. 決定如何將儲存在資料庫中的現有實體轉換為事件源聚合。過去,我們試圖將現有記錄對映到單個的、更多“屬性更改”的事件中。回想起來,我們應該已經定義了一次性轉換事件。
  16. 確定您是否希望使投影程式碼在事務上與聚合發出的事件一致,以及這是否會給您可接受的效能。如果您不這樣做,並且所有投影表都是非同步構建的,請確保程式碼庫的其餘部分不希望投影表上的查詢保持一致。
  17. 設計將現有資料轉換為新的事件源模型的策略。例如,這就是我們所做的:[list=1]
  18. 使用臨時名稱重新命名現有表及其子表
  19. 一一讀取記錄並使用您在前面步驟中設計的事件構建新的聚合
  20. 將這些新事件投影到一組新的表中,這些表的名稱和結構與遷移開始前的樣子相同
  21. 轉換和投影后立即從臨時表中刪除每條記錄
  22. 刪除臨時表
  • 對其餘實體重複前面的步驟,但不要猶豫,在生產中釋出中間步驟。
  • 根據您的需要構建更優化的投影。但不要忘記,第一個目標是轉換您現有的程式碼庫。

  • 相關文章