Greg Young在"Building an event storage"部分关于CQRS的文档中,当他向事件存储区写事件时,他检查了乐观的并发性。我不明白为什么他做了那个检查,有人能用一个具体的例子向我解释。
先感谢您。
TLDR;需要进行此并发检查,因为发出的事件取决于先前的事件。因此,如果有其他事件由另一个进程同时发出,那么必须重新做出决定。
使用Event存储的方式如下:
因此,第3步取决于执行此命令之前生成的先前事件。
如果由另一个进程并行生成的某些事件被附加到同一个事件流,那么这意味着所做的决定是基于错误的前提,因此必须通过重复从步骤1重新获取。
我不明白为什么他做了那个检查,有人能用一个具体的例子向我解释。
事件存储应该是持久的,在某种意义上说,一旦你编写了一个事件,它就会在每次后续读取时都可见。因此,数据库中的每个操作都应该是追加。一个有用的心智模型是想一个单链表。
如果数据库要支持多个具有写访问权限的执行线程,那么您将面临“丢失更新”问题。绘制为链接列表,这可能如下所示:
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)记录的事件: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的值。如果我们将写入从set
更改为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
给了我们“最后作家获胜”的语义,compare-and-set
给了我们“第一作家胜利”,这消除了失去的更新问题。