【问题标题】:Event Sourcing and when to perform validation事件溯源以及何时执行验证
【发布时间】:2021-04-28 01:01:52
【问题描述】:

我正在学习分布式系统中的事件溯源和 CQRS,我在尝试确定何时是执行验证的最佳时间时遇到了一些麻烦……在事件发生之前或之后存储?我已经对该主题进行了大量搜索和阅读,但似乎无法找到解决此问题的答案/建议。

例如(简单示例),如果我有一个 Web API 请求从银行帐户中提取一些钱,我可能会执行以下验证:

  1. 银行账户是否存在?
  2. 银行账户是否有足够的资金提取?

当请求进来时,我是在执行上述验证之前(并且有存储无效事件的风险)还是在验证之后保存事件(并且有可能在整个过程中出现问题,例如服务关闭,以及根本不存储事件)?对于 CQRS,事件是在命令执行之前存储还是作为命令的一部分(在命令处理程序中)存储?

我可以理解在发出请求之前会执行一些验证(例如,有效的提款金额),但可能存在在发出请求之前无法完成某些验证的情况。

这也导致了我如何在 Web API 调用的响应中返回错误(例如银行帐户无效)?

我对这个主题的理解可能完全错误,但正如我之前提到的,我只是在学习这个主题,我希望有人能给出答案,或者可以指出一些帖子/文章,这将有所帮助我的理解。

【问题讨论】:

    标签: .net-core cqrs event-sourcing


    【解决方案1】:

    一般来说,处理事情的有用方法是将事件定义为处理不会失败的事情(您可以通过忽略它来处理事件,但它永远不应该失败)。另一方面,命令可能会失败,或者导致零个或多个事件。

    因此,没有事件验证,只有命令。在银行账户示例中,您可以在可用资金不足的情况下拒绝提款命令,或者它可能导致提款事件和透支事件(例如,如果由此产生的透支金额在政策范围内)。

    一个组件的(我故意避免使用“服务”一词)事件可以是另一个组件的命令。

    顺便说一句,持久化处理可能失败的对象是完全有效的,但这是一种与事件溯源相关的技术,称为命令溯源;命令溯源和事件溯源通常可以有效地结合使用,尤其是当命令是其他组件的事件时。

    【讨论】:

    • +1 只是提到有一个阵营认为命令不应该失败,只是产生不同的事件。但是有很多想法,没有一种正确的方法来实现命令和事件。
    • @levi-ramsey ...我喜欢你的解释,它帮助我制定了一个我可以使用的粗略“指南”。使用我的银行示例,如果我尝试从不存在的帐户中提款,则请求将进入命令,命令验证将失败,并且不会生成/存储任何事件。但是,如果该帐户确实存在,该命令将通过验证并生成/存储一个撤销事件(假设通过了其他验证)......因此只记录永不失败的事件。
    • 为此 +1。我正在解释的一件事是写在@levi-ramsey 关于“命令采购”的评论中,命令是一等公民。当向 API 发出命令时,可能会发生几个无可辩驳的事件,例如 CommandStartedCommandFailedCommandSucceeded。 @levi-ramsey 我很想知道你为什么避免使用“服务”这个词
    • 我避免使用它,因为它在许多读者的脑海中被束缚为特定于系统的一种组件样式(例如微服务(其中很多被读入“微”) , 或具有自己的“服务”定义的各种框架)。实现组件的方法有很多种(例如,作为 Flink/Spark 中的流式作业(尽管我也使用术语“jorvice”来表示始终运行的流式作业,但这并不常见)并且对服务的含义有一个非常具体的定义可以限制为次优选择。
    【解决方案2】:

    来自Eventuous docs

    一般来说,命令处理流程可以这样描述:

    1. 边缘通过其 API(HTTP、gRPC、SignalR、消息传递等)接收命令。
    2. 它将命令传递给应用程序服务。由于边缘负责身份验证和一些授权,它可以使用用户凭据丰富命令。
    3. 与 API 本身无关的命令服务处理命令并对边缘做出响应(正面或负面)。
    4. API 层然后将响应返回给调用方。

    命令服务本身在处理一个命令时会执行以下操作:

    1. 如有必要,从命令中提取聚合 ID。
    2. 实例化所有必要的值对象。如果无法构造值对象,这可以有效地拒绝命令。命令服务还可以加载执行命令所需但不会更改状态的其他一些聚合或任何其他信息。
    3. 如果该命令希望对现有聚合实例进行操作,则会从聚合存储加载此实例。
    4. 使用命令中的值和构造的值对象对加载的(或新的)聚合执行操作。
    5. 聚合要么执行操作并通过生成新事件来更改其状态,要么拒绝操作。
    6. 如果操作成功,服务会将新事件持久保存到商店。否则,它将失败返回到边缘。

    我还将域模型不变量和验证分开。当我检查提供的字符串值是否确实是银行帐号、有效的电话号码、必填字段等时,我使用“验证”一词。域不变量,包括聚合不变量(可以处理提款)和交叉聚合不变量(一个客户的银行账户不能超过十个,真的无法想象其他任何事情)。

    如果给定帐户不存在,则为其加载聚合只会失败。但是,聚合本身可以回答以下问题:

    • 给定用户可以从该帐户中提取资金
    • 帐户是否处于活动状态,未被阻止
    • 提款金额是否在该时间段允许的提款限额内

    仅凭经验说明 - 在您创建付款时,银行很少关心您的帐户是否有钱。只有在付款被执行时才重要,而这几乎不会实时发生。

    【讨论】:

    • 感谢@Alexey-Zimarev 的回复。这是一个很好的解释。因此,如果我对您的理解正确,命令会处理验证、加载聚合等......如果一切都成功,那么事件才会记录在事件存储中?否则,命令失败(例如账户不存在)并返回响应给调用者(edge)?
    • 事件代表状态转换。这意味着您的域模型决定该操作可以执行并且一切都正确。所以,是的,你正确地描述了它。例如,当您构造值对象时,命令处理可能会失败,并且值对象逻辑说“我不能从给定的参数构造”。就像,一个电子邮件值对象检查给定的参数,如果它是一个有效的电子邮件地址。聚合保护它们自己的不变量以确保它们的状态是有效的。否则无法执行该操作,该命令将失败。
    【解决方案3】:

    事件是事实陈述,不能更改。它们代表实际发生的事情。

    您可以在命令导致一系列事件之前对其进行验证。

    既然你提到了银行账户,很多时候银行不会限制你透支你的账户。他们只是添加了一个新的事实,该事实代表了由于提款而产生的透支费用。这种情况涉及对退出事件的反应,而不是事件发生前的验证。

    【讨论】:

    • 那么您是否建议我在生成和存储任何事件之前对命令执行验证(例如银行帐户是否存在)?这意味着如果银行账户不存在,该命令将返回错误,因此不会生成/存储任何事件?
    • 没有一种方法可以做到这一点。您可以生成一个指示无效银行帐户的新事件,也可以返回错误。
    猜你喜欢
    • 2020-02-01
    • 2019-02-03
    • 2012-03-16
    • 1970-01-01
    • 2018-12-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-18
    相关资源
    最近更新 更多