从 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 生成器。并且不要忘记内部密钥与您在域外公开的密钥之间存在差异。
  11. 与此密切相关的是,在事件溯源中保证唯一性的工作方式略有不同。因此,如果您的域依赖于数据库模式来保护唯一约束,您将需要找到替代方案(例如使用流 ID)。
  12. 引入用于从/向事件存储加载和保存聚合的基础结构,并从持久化事件中重新混合聚合。您可以在此处此处此处找到一些有关如何执行此操作的示例以及 .NET 中聚合根的基类。到目前为止,我们主要使用这些参考作为示例,而不是作为框架来构建我们的域。
  13. 如果您有存储库抽象,请确保它知道哪些实体已转换并需要从事件存储中加载,哪些仍需要从原始表中加载。为此,我们使用了标记接口或 .NET 属性。
  14. 推迟诸如快照之类的决定,直到您需要它们为止。对于最终具有大量事件的聚合来说,快照是一种有效的解决方案。但是,在您获得足够的性能结果来保证这种复杂性之前,不要去那里。
  15. 决定如何将存储在数据库中的现有实体转换为事件源聚合。过去,我们试图将现有记录映射到单个的、更多“属性更改”的事件中。回想起来,我们应该已经定义了一次性转换事件。
  16. 确定您是否希望使投影代码在事务上与聚合发出的事件一致,以及这是否会给您可接受的性能。如果您不这样做,并且所有投影表都是异步构建的,请确保代码库的其余部分不希望投影表上的查询保持一致。
  17. 设计将现有数据转换为新的事件源模型的策略。例如,这就是我们所做的:[list=1]
  18. 使用临时名称重命名现有表及其子表
  19. 一一读取记录并使用您在前面步骤中设计的事件构建新的聚合
  20. 将这些新事件投影到一组新的表中,这些表的名称和结构与迁移开始前的样子相同
  21. 转换和投影后立即从临时表中删除每条记录
  22. 删除临时表
  • 对其余实体重复前面的步骤,但不要犹豫,在生产中发布中间步骤。
  • 根据您的需要构建更优化的投影。但不要忘记,第一个目标是转换您现有的代码库。