Greg Young 在他关于 CQRS 的“构建事件存储”部分的文档中,当将事件写入事件存储时,他检查了乐观并发。我真的不明白他为什么要检查,谁能用一个具体的例子向我解释。
2 回答
我真的不明白他为什么要检查,谁能用一个具体的例子向我解释。
事件存储应该是持久的,从某种意义上说,一旦您编写了一个事件,它将对随后的每次读取都是可见的。所以数据库中的每一个动作都应该是一个追加。一个有用的心智模型是考虑一个单链表。
如果数据库要支持多个具有写访问权限的执行线程,那么您将面临“丢失更新”问题。绘制为链表,可能如下所示:
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
给了我们“第一个写入者获胜”,这消除了丢失更新的担忧。
TLDR;需要此并发检查,因为发出的事件取决于先前的事件。因此,如果另一个进程同时发出其他事件,则必须重新做出决定。
事件存储的使用方式是这样的:
- 旧事件从 Eventstream 加载(= Eventstore 中的一个分区,其中包含由 Aggregate 实例生成的所有事件)
- 旧事件由拥有它们的聚合按它们生成的顺序处理/应用
- 基于从这些事件构建的内部状态,聚合决定发出一些新事件
- 这些新事件被附加到 Eventstream
因此,步骤 3 取决于在执行此命令之前生成的先前事件。
如果另一个进程并行生成的一些事件被附加到同一个 Eventstream 中,那么这意味着做出的决定是基于错误的前提,因此必须通过从步骤 1 重复来重新做出决定。