在DDD,CQRS,EventSourcing中有错误的,不一致的事件流中发生逻辑异常吗?

时间:2018-08-30 23:38:03

标签: exception exception-handling domain-driven-design cqrs event-sourcing

假设您通过EventSourcing处理DDD。

我们都知道事件是不可变的,因此永远不要从事件日志中删除它们。但是,如果流在逻辑上是“不正确的”怎么办?不是那种经典的情况:“我加钱了,我不必加钱,所以创建一个补偿事件来取钱。”

我不是在谈论运行时异常,而是在事件流中发现的逻辑异常,因为编码人员在事件编写器中犯了错误。

问题

如果编写事件流的软件包含违反域逻辑的错误,您如何“重播”事件流?

Oookay ...我们都知道,“ 应该从未发生过”和“解雇编写这些事件编写器的编码器”等等……

但是我们假设事件流 就在那儿,并且您正在重建所有流的投影重放。只是可能已经发生,然后告诉您从现有事件流中重建预测。

突然,当重放事件流时,您会发现不符合当前业务规则或当时存在的规则的“不连贯”事件。

示例1

您有以下事件:

#  TimeStamp  Event          Data
------------------------------------------------------
1  03/jul     car.created    { id: 4444, color: blue }
2  14/jul     car.delivered  { id: 4444, to: Alice }
3  18/jul     car.created    { id: 5555, color: blue }
4  22/jul     car.created    { id: 5566, color: orange }
5  25/jul     car.created    { id: 5577, color: blue }

7月26日,有人问:“您有多少辆蓝色汽车?”

透明晶体:2个单位(标识55555577)。

原因:单位4444已售出。单位5566为橙色。

但是,如果您有这个越野车序列怎么办?

#  TimeStamp  Event          Data
------------------------------------------------------
1  03/jul     car.created    { id: 4444, color: blue }
2  14/jul     car.delivered  { id: 4444, to: Alice }
3  18/jul     car.created    { id: 5555, color: blue }
4  22/jul     car.created    { id: 5566, color: orange }
5  23/jul     car.created    { id: 5555, color: red }
6  25/jul     car.created    { id: 5577, color: blue }

当然,事件5永远不会发生,您不能创建相同的单元2次。

调查领域专家后,您发现事件5不正确。它应该显示为“ car.repainted”,但该软件存在错误,并编写了“ car.created”。

示例1的问题:

  • 您是否要添加编号为7或更多的新事件,并在事件5之后加上时间戳,以进行某种补偿?您会写哪些事件?
  • 您是否要添加新事件7和更多事件,并在事件5之前加时间戳记,以便向重播者“嘿,忽略下一个创建”发出某种信号?您会写哪些事件?
  • 您是否会重写“重播器”,以便他们可以解释为“ 25 / jul之前的任何东西都是“双重创作”的意思是“ car.repainted”,然后重新运行重播器以重建聚合?
  • 您会违反黄金法则并“触摸”历史吗?实际上,这不是“历史”,因为事件“ 5”并未真正发生。那我们可以触摸它吗?

示例2

让我们假设一个仓库里有叉车来从架子上取东西。仓库包含2条垂直走廊,2条水平走廊和1条对角走廊。

所有走廊都是双向的,除了左垂直的走廊上有某种台阶或类似的东西外,叉车只能从A移到C,而不能反向移动。并且从下面的水平线也有台阶,并且叉车只能从D移至C,而不能从C移至D。

购买机器后,每天从仓库A的进口门开始在A点。无论这个例子在一天结束时叉车如何消失,都不在乎。

命令可以是:

purchase()
start()
goRight()
goLeft()
goUp()
goDown()
cross()

事件可以是:

purchased
started
wentRight
wentLeft
wentUp
wentDown
crossed

这是叉车集合体的可能状态图:

eventsourcing aggregate state diagram

让我们假设您正在重放聚合的事件,并且发现了这些事件:

#   TimeStamp     Event
----------------------------------------------
1   12/jul 10:00  purchased
2   14/jul 09:00  started
3   14/jul 11:00  wentDown
4   14/jul 12:00  crossed
5   14/jul 14:00  wentDown
6   23/jul 09:00  started
7   23/jul 10:00  wentRight
8   23/jul 13:00  crossed

有人问“现在叉车在哪里?您可以轻松地告诉“ C”。

原因:无论6之前发生了什么,因为事件6重置为位置A,事件7都移向B,事件{{1} }向8移动。

但是,如果序列继续这样下去怎么办?

C

一些领域专家问您:“嘿,极客,您告诉我们事件外包非常神奇:7月23日18:00的叉车在哪里?”

我们都知道电梯不能在楼梯上“跳跃”,所以我们都知道事件9永远不会发生。

因此,我们的“重播器”无法做其他引发异常的事情。但是已经编写的事件序列就是那个。

这里的主题不是“如何编写一个好的序列”,而是“面对异常序列时该怎么做”。

示例2的问题:

  • 您会写一个补偿事件吗?怎么样?哪一个?什么时候?
  • 您会重写历史记录吗? (如果您有数百万个事件,那很丑)
  • 从域事件 replayers 的角度来看,您将如何处理该异常?

1 个答案:

答案 0 :(得分:2)

  

如果编写事件流的软件包含违反域逻辑的错误,您如何“重播”事件流?

您至少有两个选择:

  1. 在汇总apply方法或事件订阅者(Readmodels,projections,Sagas)中放入一些修复代码;这段代码应该处理您要避免的确切情况。

它的缺点是它将永远存在于代码库中,但是它的优点是可以在零停机时间内完成。

  1. 迁移事件存储。格雷格·杨(Greg Young)关于如何做到这一点的问题{@ {3}}。基本上,您可以在可能的另一个事件存储实例上创建另一个事件流,处理该事件流中的每个事件,修复异常并追加到新的事件流中。迁移完成后,您将旧的事件存储替换为新的事件存储。

它的缺点是替换事件存储区时可能需要停机,但是这样做的好处是您可以“忘记”错误,从而获得干净/正确的事件流。

  

您会写一个补偿事件吗?怎么样?哪一个?什么时候?

想要快速解决方案时,编写补偿事件很方便;这是解决方案编号的特例。 1。

  

您会违反黄金法则并“触摸”历史吗?实际上,这不是“历史”,因为事件“ 5”并未真正发生。那我们可以触摸吗?

您可以做到这一点,我确实做到了,因为我想要尽可能最快的解决方案,但是根据框架/技术的不同,它可能很难看。例如,订阅者无法再确定他们是否处理了事件存储中的所有相关事件,因此,为了确定,您需要重建所有Readmodels; Sagas可能会遇到的最大问题是,处理事件会产生副作用。

关于法律方面,如果您确实修改了历史记录,则需要存档旧的事件流,以防万一有人要它。这取决于您的域。