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

事件溯源是高性能協作域的一種很好的架構風格,可以保證它增加的複雜性。但正如我之前所說,就像任何其他原則或實踐一樣,即使是事件溯源也有利有弊。而且它不是頂級架構。您系統的某些部分可能會從中受益,但其他部分可能不會。話雖如此,如果您需要事件溯源,並且您有一個現有的、更傳統的(又名 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 生成器。

並且不要忘記內部密鑰與您在域外公開的密鑰之間存在差異。
  1. 與此密切相關的是,在事件溯源中保證唯一性的工作方式略有不同。
因此,如果您的域依賴於數據庫模式來保護唯一約束,您將需要找到替代方案(例如使用流 ID)。
  1. 引入用於從 / 向事件存儲加載和保存聚合的基礎結構,並從持久化事件中重新混合聚合。
您可以在此處、此處和此處找到一些有關如何執行此操作的示例以及 .NET 中聚合根的基類。

到目前爲止,我們主要使用這些參考作爲示例,而不是作爲框架來構建我們的域。
  1. 如果您有存儲庫抽象,請確保它知道哪些實體已轉換並需要從事件存儲中加載,哪些仍需要從原始表中加載。
爲此,我們使用了標記接口或 .NET 屬性。
  1. 推遲諸如快照之類的決定,直到您需要它們爲止。
對於最終具有大量事件的聚合來說,快照是一種有效的解決方案。

但是,在您獲得足夠的性能結果來保證這種複雜性之前,不要去那裏。
  1. 決定如何將存儲在數據庫中的現有實體轉換爲事件源聚合。
過去,我們試圖將現有記錄映射到單個的、更多 “屬性更改” 的事件中。

回想起來,我們應該已經定義了一次性轉換事件。
  1. 確定您是否希望使投影代碼在事務上與聚合發出的事件一致,以及這是否會給您可接受的性能。
如果您不這樣做,並且所有投影表都是異步構建的,請確保代碼庫的其餘部分不希望投影表上的查詢保持一致。
  1. 設計將現有數據轉換爲新的事件源模型的策略。
例如,這就是我們所做的:

[list=1]
  1. 使用臨時名稱重命名現有表及其子表

  2. 一一讀取記錄並使用您在前面步驟中設計的事件構建新的聚合

  3. 將這些新事件投影到一組新的表中,這些表的名稱和結構與遷移開始前的樣子相同

  4. 轉換和投影后立即從臨時表中刪除每條記錄

  5. 刪除臨時表

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/LD8uU9yDzJWd9sEH3WM-Iw