ES / CQRS并发处理


20

我最近开始研究CQRS / ES,因为我可能需要在工作中应用它。对于我们来说,这似乎很有希望,因为它将解决很多问题。

我对ES / CQRS应用程序如何看起来像上下文简化后的银行业务用例(提取资金)进行了粗略的了解。

ES / CQRS

综上所述,如果A人提取了一些钱:

  • 发出命令
  • 命令已交出以进行验证/验证
  • 如果验证成功,则将事件推送到事件存储
  • 聚合器使事件出队以对聚合应用修改

据我了解,事件日志是事实的源泉,因为它是FACTS的日志,因此我们可以从中得出任何预测。


现在,在这种宏伟的计划中,我不了解的是在这种情况下会发生什么:

  • 规则:余额不能为负
  • A人的余额为100e
  • 人A发出100e的WithdrawCommand
  • 验证通过并发出100e事件的MoneyWithdrewEvent
  • 同时,人A发出另一个100e的WithdrawCommand
  • 第一个MoneyWithdrewEvent尚未汇总,因此验证通过,因为针对汇总(尚未更新)的验证检查
  • 再次发出100e的MoneyWithdrewEvent

==>我们处于不一致状态,余额为-100e,并且日志包含2 MoneyWithdrewEvent

据我了解,有几种策略可以解决此问题:

  • a)将聚合版本ID和事件一起放入事件存储中,因此,如果修改后版本不匹配,则不会发生任何事情
  • b)使用一些锁定策略,这意味着验证层必须以某种方式创建一个

与策略有关的问题:

  • a)在这种情况下,事件日志不再是事实的源头,如何处理?另外,我们还给客户端确定,但是允许退出是完全错误的,在这种情况下使用锁是否更好?
  • b)锁==死锁,您对最佳做法有什么见解?

总体而言,我对如何处理并发的理解正确吗?

注意:我了解同一个人不可能在如此短的时间内提取两次资金,但是我举了一个简单的例子,不要迷失细节


为什么不在第4步中更新聚合而不是等到第7步?
艾里克·艾德

因此,您的意思是在这种情况下,事件存储只是一个日志,仅在启动应用程序以重新创建聚合/其他投影时才会读取?
路易F.17年

Answers:


19

我对ES / CQRS应用程序如何看起来像上下文简化后的银行业务用例(提取资金)进行了粗略的了解。

这是事件源应用程序的完美示例。开始吧。

每次处理或重试命令(您将理解,请耐心等待),将执行以下步骤:

  1. 命令到达命令处理程序,即中的服务Application layer
  2. 命令处理程序的标识Aggregate由进行装载从储存库和负载它(在这种情况下new-ing一个Aggregate实例,获取这个聚集体和所有先前发出的事件重新将其应用到团聚物本身;总结版本被存储用于以后使用;在应用事件后,汇总处于最终状态-即,当前帐户余额以数字形式计算)
  3. 命令处理程序在上调用适当的方法Aggregate,例如,Account::withdrawMoney(100)并收集产生的事件,即MoneyWithdrewEvent(AccountId, 100); 如果帐户中没有足够的钱(余额<100),则会引发异常,并且所有异常终止;否则,执行下一步。
  4. 命令处理程序尝试将持久化Aggregate到存储库(在这种情况下,存储库是Event Store);它通过附加新事件这样做Event stream,当且仅当versionAggregate仍是一个当这是Aggregate加载。如果版本不同,则重试该命令-转到步骤1。如果version相同,则将事件附加到,Event stream并为客户端提供Success状态。

此版本检查称为乐观锁定,是一种常规锁定机制。另一种机制是悲观锁定,即当其他作品被阻止直到当前作品完成时才被悲观锁定

该术语Event stream是围绕同一聚合发出的所有事件的抽象。

您应该了解,这Event store只是将所有更改存储到聚合(而不是最终状态)的另一种持久性。

a)在这种情况下,事件日志不再是事实的源头,如何处理?另外,我们还给客户端确定,但是允许退出是完全错误的,在这种情况下使用锁是否更好?

事件存储始终是真理的源头。

b)锁==死锁,您对最佳做法有什么见解?

通过使用开放式锁定,您没有锁定,只需命令重试即可。

无论如何,Locks!=死锁


2
对于不加载Aggregate所有事件的加载有一些优化,但是您保留了Aggregate过去某个时间点的快照,并且仅应用了该时间点之后发生的事件。
康斯坦丁·加尔贝努

好的,我认为我的困惑来自于事件存储==事件总线(我想到了kafka)的事实,因此重新构造聚合可能很昂贵,因为您可能需要重新读取很多事件。如果有的快照Aggregate,应何时更新快照?快照存储与事件存储相同还是从事件总线派生的物化视图?
路易F.17年

有一些创建快照的策略。一种是每n个事件制作一个快照。您应该在同一提交上将快照以及事件存储在相同的位置/持久性/数据库中。想法是快照与聚合的版本密切相关。
康斯坦丁·加尔贝努

好的,我认为我对如何处理此问题有更清晰的认识。现在最后一个问题是,事件总线的作用到底是什么?聚合是否同步更新?
路易F.17年

1
是的,您可以使用RabbitMQ或任何想要将事件异步发送到读取模型的通道,但前提是将它们持久保存到事件存储中。确实,持久化事件之后不会进行事件验证:事件代表发生的事实;事件代表发生的事实。读取模型可能会或可能不会喜欢发生了某些事情,但是它无法更改历史记录。
康斯坦丁·加尔贝努

1

我对ES / CQRS应用程序如何看起来像上下文简化后的银行业务用例(提取资金)进行了粗略的了解。

关。问题在于,用于更新“聚合”的逻辑位置很奇怪。

更常见的实现是命令处理程序将数据模型保存在内存中,并且事件存储中的事件流保持同步。

一个容易描述的示例是命令处理程序对事件存储进行同步写入,并在与事件存储的连接指示成功写入的情况下更新其模型的本地副本的情况。

如果命令处理程序需要与事件存储重新同步(因为其内部模型与存储的内部模型不匹配),则可以通过从存储中加载历史记录并重建其内部状态来做到这一点。

换句话说,箭头2&3(如果存在)通常将连接到事件存储,而不是聚合存储。

将聚合版本ID和事件一起放入事件存储中,因此,如果修改时版本不匹配,则不会发生任何事情

这种变化是在通常情况下-而不是附加到事件流中的流,我们通常PUT到流中的特定位置; 如果该操作与存储状态不兼容,则写入失败,并且服务可以选择适当的失败模式(客户端失败,重试,合并...)。使用幂等写入解决了分布式消息传递中的许多问题,但是当然,它确实需要拥有支持幂等写入的存储。


嗯,我想我当时误解了事件存储组件。我认为一切都应该经过它并得到流式传输。如果我的活动存储区是kafka并且是只读的怎么办?我负担不起步骤2和3再次浏览所有消息。看来,我的整体愿景相匹配这一个:medium.com/technology-learning/...
路易斯F.
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.