Greg Young在"Building an event storage"部分中有关CQRS的文档中,当将事件写入事件存储时,他检查了乐观并发性。我真的不明白他为什么做这张支票,任何人都可以用一个具体的例子向我解释。
谢谢。
答案 0 :(得分:3)
TLDR;需要进行并发检查,因为发出什么事件取决于先前的事件。因此,如果其他进程同时发出其他事件,则必须重新做出决定。
事件存储的使用方式如下:
因此,步骤3取决于执行此命令之前产生的先前事件。
如果将由另一个进程并行生成的某些事件附加到同一Eventstream中,则意味着所做出的决定是基于错误的前提,因此必须从步骤1开始重复进行。
答案 1 :(得分: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])
线程(2)编写的历史记录不包括线程(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])
这里的问题很简单,就是线程(2)认为6 <- /x.tail
是正确的,并用丢失事件7的值替换了它。如果我们将写从set
更改为{{1 }} ...
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
给予我们“最后一位作家获胜”的语义,set
给予我们“第一位作家获胜”的语义,从而消除了对更新丢失的担忧。