14

问题:

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

场景:

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

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

...然后他们都重新上线,第一个员工 A 追加,从而将客户更改为版本 #21,然后是员工 B,仍然在版本 #20

问题:

我们在场景 1 中保留谁的更改?

我们可以在场景 2 中进行合并,如何?

语境:

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

2 - 使用事件溯源数据库作为队列

3 - 读取模型的最终一致性

4 - RESTful API

视觉倾斜图; 它是 MS 图的混搭,并且发生了一些变化

EDIT-1:根据迄今为止的答案进行澄清:

例如,为了执行细粒度合并,我需要为表单中的每个字段设置一个命令?

在此处输入图像描述

上面,ChangeName、ChangeSupplier、ChangeDescription 等的细粒度命令,每个都有自己的时间戳,将允许在事件 A 和 B 都更新 ChangedName 时自动合并?

Edit-2:根据特定事件存储的使用情况进行跟进:

似乎我将使用 @GetEventStore 来持久化我的事件流。

他们使用乐观并发如下:

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

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

    • -1 指定流不应该已经存在

    • 0及以上指定流版本

    • 如果流不是该版本,则写入将失败,您可以使用新的预期版本号重试,或者您重新处理该行为并决定如果您这样选择就可以了。

  • 如果未指定 ES-Expected Version,则禁用乐观并发控制

  • 在这种情况下,Optimistic Concurrency 不仅基于 Message ID,还基于 Event #

4

4 回答 4

5

如果我正确理解你的设计图,那么偶尔连接的用户排队命令,即更改请求,当用户重新连接时,排队的命令会一起发送;只有一个数据库权限(命令处理程序查询以加载其聚合的最新版本);只有视图模型同步到客户端。

在此设置中,场景 2由您的设计自动合并,如果您明智地选择命令,请阅读:使它们细粒度:对于每个可能的更改,选择一个命令。然后,在客户端重新连接时,命令以任何顺序处理,但由于它们只影响分离字段,所以没有问题:

  1. 客户是 v20。
  2. A 处于离线状态,针对 v20 的陈旧模型编辑更改。
  3. B 离线,针对 v20 的陈旧模型编辑更改。
  4. A上线,批量发送排队ChangeName命令,v20的Customer被加载并持久化为v21。
  5. B上线,批量发送排队ChangeAddress命令,v21的Customer被加载并持久化为v22。
  6. 正如预期的那样,数据库包含具有正确姓名和地址的用户。

场景 1中,使用此设置,两名员工都将覆盖其他员工的更改:

  1. 客户是 v20。
  2. A 处于离线状态,针对 v20 的陈旧模型编辑更改。
  3. B 离线,针对 v20 的陈旧模型编辑更改。
  4. A上线,批量向“John Doe”发送排队ChangeName命令,v20的客户被加载并保存为v21,名称为“John Doe”
  5. B 上线,批量向“Joan d'Arc”发送排队ChangeName命令,v21 的客户(名为“John Doe”)被加载并保存为 v22(名为“Joan d'Arc”)。
  6. 数据库包含一个名为“Joan d'Arc”的用户。

如果 B 在 A 之前上线,则反之亦然:

  1. 客户是 v20。
  2. A 处于离线状态,针对 v20 的陈旧模型编辑更改。
  3. B 离线,针对 v20 的陈旧模型编辑更改。
  4. B 上线,批量向“Joan d'Arc”发送排队ChangeName命令,v20 的 Customer 被加载并持久化为 v21(名称为“Joan d'Arc”)。
  5. A 上线,批量向“John Doe”发送排队ChangeName命令,v21 的客户被加载并保存为 v22,名称为“John Doe”。
  6. 数据库包含一个名为“John Doe”的用户。

有两种方法可以启用冲突检测:

  1. 检查该命令的创建日期(即员工修改时间)是否Customer. 这将禁用场景 2 的自动合并功能,但会为您提供针对并发编辑的完整冲突检测。
  2. 检查该命令的创建日期(即员工修改时间)是否在要更改的单个字段的最后一次修改日期之后Customer。这将使场景 2 的自动合并保持不变,但会在场景 1 中为您提供自动冲突检测。

两者都很容易通过事件溯源实现(因为事件流中各个事件的时间戳可能是已知的)。

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

EDIT-1:回答澄清问题:

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

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

于 2014-08-11T12:50:18.430 回答
4

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

方案 1

必须有人做出决定,最好是人。您应该询问用户或表明存在冲突。

Dropbox 通过选择后面的文件并将 file.conflict 文件保存在同一目录中供用户删除或使用来解决此问题。

方案 2

保留原始数据并查看实际更改的字段。然后,您可以应用员工 1 的更改,然后应用员工 2 的更改,而无需踩到任何脚趾。

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

让第二个用户知道他们离线时发生了变化。尝试场景 2 并向第二个用户显示新结果(因为这可能会改变他的输入)。然后问他是否要保存更改、先修改还是丢弃。

于 2014-08-10T00:33:40.557 回答
0

Aaron,其中事件确实发生冲突,即在场景 1 中,我希望会引发某种并发异常。

第二种情况更有趣。假设您的命令和事件定义合理,即不是 CRUD 的包装器,那么您将能够测试自发出命令以来提交的事件是否实际冲突。为此,我使用并发冲突注册表。本质上,当我检测到潜在的冲突时,我会抓取自我当前拥有的版本以来已提交的事件,并要求注册表检查其中是否有任何实际冲突。

如果您想查看代码示例以及更多详细信息,我会整理一篇文章,概述我的方法。看这里:handling concurrency issues in cqrs es systems

希望这可以帮助!

于 2014-09-29T10:34:15.630 回答
0

在这种情况下,也许您可​​以使用“聚合根”的概念,为由 CEP 引擎(复杂事件处理引擎)提供支持的项目来执行这些复杂的操作。

于 2016-10-25T08:43:22.763 回答