偶尔连接的CQRS系统

14

问题:

两名员工(A和B)在同时编辑客户#123时下线,例如版本#20,并在离线期间继续进行更改...

场景:

1-两名员工编辑客户#123并对一个或多个相同属性进行更改。

2-两名员工编辑客户#123但未进行相同的更改(他们互相交叉而不接触)。

...然后他们都回到在线状态,第一名员工A附加,从而将客户更改为版本#21,然后是仍处于版本#20的员工B

问题:

在情况1中,我们保留谁的更改?

在情况2中,我们能合并吗?如何操作?

背景:

1- CQRS +事件溯源风格系统

2-使用事件溯源Db作为队列

3-读模型的最终一致性

4-RESTful API

面向视觉的图表;这是微软图表的组合,有些内容被修改

编辑-1:根据目前的答案进行澄清:

为了执行细粒度合并,我需要为表单中的每个字段都有一个命令,例如ChangeName,ChangeSupplier,ChangeDescription等吗?

输入图像说明

上面,对于ChangeName,ChangeSupplier,ChangeDescription等的详细命令,每个命令都有自己的时间戳,这将允许在A&B同时更新ChangedName时进行自动合并?

编辑-2:基于使用特定事件存储的后续操作:

看起来我将使用@GetEventStore来持久化我的事件流。

他们如下使用乐观并发:

  • 每个流中的事件都会将流版本号增加1。

  • 写入操作可以指定期望版本,利用写入器上的ES-ExpectedVersion标头

    • -1表示流不应该已经存在

    • 0及以上表示一个流的版本号

    • 如果流不在该版本,则写入操作将失败,您可以使用新的期望版本号重试或重新处理行为,并决定是否OK。

  • 如果没有指定ES-Expected Version,乐观并发控制将被禁用

  • 在这种情况下,乐观并发不仅基于消息ID,还基于事件编号


提醒一下,我在这里问了另一个与此相关的问题:http://stackoverflow.com/questions/25257013/occasionally-connected-cqrs-systems-client-and-server-commands-task-based-sc - Aaron
1
对于EDIT-2:我不熟悉GetEventStore,但是ES-ExpectedVersion应该由命令处理程序指定,基于它从数据库加载的聚合版本。通常情况下,您应该保留它。您的命令本身通常不会包含特定版本,因为命令代表用例,并且应该忽略底层域模型(因此,特定聚合的版本)。 - Alexander Langer
我想在我们的系统中使用完全相同的模型,有哪些阅读材料可以讨论将命令转换为事件,以及可能的离线方面? - Joel
4个回答

5
如果我正确理解你的设计图片,那么偶尔连接的用户会排队命令(即更改请求),当用户重新连接时,排队的命令一起发送;只有一个数据库权限(命令处理程序查询以加载其聚合物最新版本的数据库),只有视图模型与客户端同步。
在这种设置中,如果您明智地选择命令,也就是制定了细粒度的命令,那么“场景2”将被您的设计自动合并:对于每个可能的更改,请选择一个命令。然后,在客户端重新连接时,命令将按任意顺序处理,但因为它们只影响不相交的字段,所以没有问题:
1. 客户端处于v20状态。 2. A已离线,针对v20的旧模型进行编辑更改。 3. B已离线,针对v20的旧模型进行编辑更改。 4. A上线,批处理发送一个排队的ChangeName命令,将v20的客户端加载并持久化为v21。 5. B上线,批处理发送一个排队的ChangeAddress命令,将v21的客户端加载并持久化为v22。 6. 数据库中包含具有正确名称和地址的用户,如预期的那样。
在“场景1”中,使用此设置,两个员工都会覆盖另一个员工的更改:
1. 客户端处于v20状态。 2. A已离线,针对v20的旧模型进行编辑更改。 3. B已离线,针对v20的旧模型进行编辑更改。 4. A上线,批处理发送一个排队的ChangeName命令到“John Doe”,将v20的客户端加载并持久化为v21,并以名称“John Doe”。 5. B上线,批处理发送一个排队的ChangeName命令到“Joan d'Arc”,将v21(名为“John Doe”的客户)的客户端加载并持久化为v22(名称为“Joan d'Arc”)。 6. 数据库包含一个姓名为“Joan d'Arc”的用户。
如果B先于A上线,则反之亦然:
1. 客户端处于v20状态。 2. A已离线,针对v20的旧模型进行编辑更改。 3. B已离线,针对v20的旧模型进行编辑更改。 4. B上线,批处理发送一个排队的ChangeName命令到“Joan d'Arc”,将v20的客户端加载并持久化为v21(名称为“Joan d'Arc”)。 5. A上线,批处理发送一个排队的ChangeName命令到“John Doe”,将v21的客户端加载并持久化为v22,并以名称“John Doe”。 6. 数据库包含一个姓名为“John Doe”的用户。
有两种启用冲突检测的方法:
  1. 检查命令的创建日期(即员工修改的时间)是否在Customer的最后修改日期之后。这将禁用方案2的自动合并功能,但会提供完整的冲突检测以防止并发编辑。
  2. 检查命令的创建日期(即员工修改的时间)是否在要更改的Customer单个字段的最后修改日期之后。这将保留方案2的自动合并功能,但会在方案1中提供自动冲突检测。

这两种方法都很容易使用事件源实现(因为事件流中每个事件的时间戳可能已知)。

至于你的问题“在方案1中我们保留谁的更改?”--这取决于您的业务领域及其需求。

编辑-1:回答澄清问题:

是的,您需要为每个可以单独更改的字段(或字段组)编写一个命令。

关于您的模型:您展示的是典型的“CRUD” UI,即多个表单字段和一个“保存”按钮。 CQRS通常和“基于任务”的UI自然结合,例如,Status字段被显示(只读),如果用户想要更改状态,则点击“更改状态”按钮,打开对话框/新窗口或其他UI元素,在那里可以更改状态(在基于Web的系统中,原地编辑也很常见)。如果您正在使用“基于任务”的UI,其中每个任务仅影响所有字段的一小部分,则ChangeName、ChangeSupplier等细粒度命令是自然适合的。


谢谢您的回答。我会接受它,但我编辑了我的问题以请求澄清。 - Aaron

4
以下是一些解决方案的概述:

情景1

需要有人做出决定,最好是人类。您应该询问用户或显示存在冲突。

Dropbox通过选择后期文件并在同一目录中保留一个file.conflict文件来解决这个问题,让用户删除或使用该文件。

情景2

保留原始数据,并查看哪些字段实际上发生了变化。然后,您可以应用员工1的更改,然后再应用员工2的更改,而不会产生任何问题。

情景3(仅当更改在不同时间在线时)

让第二个用户知道他们离线时曾经有更改。尝试使用情景2,并向第二个用户展示新结果(因为这可能会改变他的输入)。然后询问他是否要保存自己的更改,首先修改它们,还是放弃它们。


0

Aaron,如果事件确实发生了冲突,比如在场景1中,我希望会抛出某种并发异常。

第二个场景更有趣。假设您的命令和事件已经合理地定义,即不是CRUD的包装器,那么您将能够测试自从发出命令以来提交的事件是否真正冲突。我使用并发冲突注册表来实现这个目的。基本上,当我检测到潜在的冲突时,我获取当前版本以来已经提交的事件,并要求注册表检查它们是否真正冲突。

如果您想看一个代码示例,并且想要更多关于此的详细信息,我编写了一篇文章概述了我的方法。请在这里查看:处理cqrs es系统中的并发问题

希望这可以帮助您!


0
在这种情况下,也许您可以使用“聚合根”概念,针对由CEP引擎(复杂事件处理引擎)驱动的项目来执行这些复杂操作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接