5

Greg Young 在他关于 CQRS 的“构建事件存储”部分的文档中,当将事件写入事件存储时,他检查了乐观并发。我真的不明白他为什么要检查,谁能用一个具体的例子向我解释。

4

2 回答 2

5

我真的不明白他为什么要检查,谁能用一个具体的例子向我解释。

事件存储应该是持久的,从某种意义上说,一旦您编写了一个事件,它将对随后的每次读取都是可见的。所以数据库中的每一个动作都应该是一个追加。一个有用的心智模型是考虑一个单链表。

如果数据库要支持多个具有写访问权限的执行线程,那么您将面临“丢失更新”问题。绘制为链表,可能如下所示:

Thread(1) [... <- 69726c3e <- /x.tail] = get(/x)
Thread(2) [... <- 69726c3e <- /x.tail] = get(/x)
Thread(1) set(/x, [ ... <- 69726c3e <- 709726c3 <- /x.tail])
Thread(2) set(/x, [ ... <- 69726c3e <- 83b97195 <- /x.tail])

thread(2) 写入的历史不包括 thread(1) 记录的 event:709726c3。因此“丢失更新”。

在通用数据库中,您通常使用事务来管理它:幕后的一些魔法会跟踪您所有的数据依赖关系,如果在您尝试提交事务时前提条件不成立,那么您的所有工作都会被拒绝。

但是事件存储并不需要支持一般情况的所有自由度——禁止编辑存储在数据库中的事件,以及更改事件之间的依赖关系。

更改的唯一可变部分——这是我们用新值替换覆盖旧值的唯一地方——是我们更改的时候/x.tail

Thread(1) [... <- 69726c3e <- /x.tail] = get(/x)
Thread(2) [... <- 69726c3e <- /x.tail] = get(/x)
Thread(1) set(/x, [ ... <- 69726c3e <- 709726c3 <- /x.tail])
Thread(2) set(/x, [ ... <- 69726c3e <- 83b97195 <- /x.tail])

这里的问题只是 Thread(2) 认为6 <- /x.tail是正确的,并将其替换为丢失事件 7 的值。如果我们将写入从 aset更改为compare-and-set...

Thread(1) [... <- 69726c3e <- /x.tail] = get(/x)
Thread(2) [... <- 69726c3e <- /x.tail] = get(/x)
Thread(1) compare-and-set(/x, 69726c3e <- /x.tail, [ ... <- 69726c3e <- 709726c3 <- /x.tail])
Thread(2) compare-and-set(/x, 69726c3e <- /x.tail, [ ... <- 69726c3e <- 83b97195 <- /x.tail]) // FAILS

然后数据存储可以检测到冲突并拒绝无效写入。

当然,如果数据存储以不同的顺序看到线程的操作,那么失败的命令可能会改变

Thread(1) [... <- 69726c3e <- /x.tail] = get(/x)
Thread(2) [... <- 69726c3e <- /x.tail] = get(/x)
Thread(2) compare-and-set(/x, 69726c3e <- /x.tail, [ ... <- 69726c3e <- 83b97195 <- /x.tail])
Thread(1) compare-and-set(/x, 69726c3e <- /x.tail, [ ... <- 69726c3e <- 709726c3 <- /x.tail]) // FAILS

更简单地说,whereset给了我们“最后一个写入者获胜”的语义,compare-and-set给了我们“第一个写入者获胜”,这消除了丢失更新的担忧。

于 2018-09-13T11:52:22.593 回答
4

TLDR;需要此并发检查,因为发出的事件取决于先前的事件。因此,如果另一个进程同时发出其他事件,则必须重新做出决定。

事件存储的使用方式是这样的:

  1. 旧事件从 Eventstream 加载(= Eventstore 中的一个分区,其中包含由 Aggregate 实例生成的所有事件)
  2. 旧事件由拥有它们的聚合按它们生成的顺序处理/应用
  3. 基于从这些事件构建的内部状态,聚合决定发出一些新事件
  4. 这些新事件被附加到 Eventstream

因此,步骤 3 取决于在执行此命令之前生成的先前事件。

如果另一个进程并行生成的一些事件被附加到同一个 Eventstream 中,那么这意味着做出的决定是基于错误的前提,因此必须通过从步骤 1 重复来重新做出决定。

于 2018-09-13T11:55:27.417 回答