我正在寻求与在CQRS/事件溯源应用程序中实现回滚功能的正确方式相关的建议。
该应用程序允许一组编辑人员编辑和更新某些编辑内容,例如编辑新闻。我们已经实现了用户界面,使得每个字段都具有自动保存功能,现在我们希望为用户提供撤销操作的可能性,以便可以将编辑新闻回滚到先前已知的状态。
基本上,我们希望实现类似于Microsoft Word和类似文本编辑器中拥有的撤销命令。在后端,编辑新闻是在我们的领域中定义的聚合的一个实例,称为Story。
我们已经讨论了一些实现回滚的想法,并且正在寻求基于类似项目的真实世界经验的建议。以下是我们对此功能的考虑。
在现实世界的业务领域中如何实现回滚
首先,我们都知道,在现实世界的业务领域中,我们所说的回滚是通过某种形式的补偿事件获得的。
想象一下与某种可购买订阅的服务相关的领域:我们可以有一个表示用户订阅的聚合,并且描述一个收费已与聚合的实例(即某个客户的特定订阅)相关联的事件。该事件的可能实现如下:
public class ChargeAssociatedToSubscriptionEvent: DomainEvent
{
public Guid SubscriptionId {get; set;}
public decimal Amount {get; set;}
public string Description {get; set;}
public DateTime DueDate {get; set;}
}
如果一笔费用错误地关联到了一个订阅,可以通过与相同订阅和同样金额相关联的认证来修复错误,以便使费用的影响完全平衡,并且用户能够拿回他的钱。换句话说,我们可以定义以下补偿事件:
public class AccreditationAssociatedToSubscription: DomainEvent
{
public Guid SubscriptionId {get; set;}
public decimal Amount {get; set;}
public string Description {get; set;}
public DateTime AccreditationDate {get; set;}
}
如果用户被错误收取了50美元的费用,我们可以通过将50美元的授信归还给用户订阅来补偿错误:这样聚合状态就回滚到了先前的状态。
为什么事情并不像看起来那么简单
根据之前的讨论,回滚似乎很容易实现。如果你有一个聚合修订版本B的实例并且想要将其回滚到以前的聚合修订版本A(其中A < B),你只需要执行以下步骤:
- 检查事件存储并获取A和B之间发生的所有事件
- 计算每个已发生事件的补偿事件
- 按相反的顺序将补偿事件应用于聚合
不幸的是,上述过程的第二步并不总是可能的:给定通用领域事件时,并不总是能够计算出其补偿事件,因为该事件中包含的信息量可能不足以进行计算。也许可以明智地定义所有事件,使它们包含足够的信息,以能够计算出相应的补偿事件,但在我们应用程序的当前状态下,存在几个无法计算补偿事件的事件,我们更愿意避免更改事件形状。
一种基于状态比较的可能解决方案
克服补偿事件问题的第一个想法是通过比较聚合当前状态与目标状态,计算回滚聚合所需的最小事件集。基本算法如下:
- 获取聚合在当前状态下的实例(称为B)
- 仅应用保存在事件存储器中内的前n个事件,获取聚合在目标状态下的实例(称为A)(我们的存储库允许通过指定聚合ID和要将聚合材料化到的期望时间点来执行此操作)
- 比较两个实例并计算要应用于状态B的聚合的最小事件集,以使其状态为A
- 将计算出的事件应用于聚合
基于事件重放的更智能方法
解决回滚聚合到先前状态的问题的另一种方法可能是做与聚合存储库在特定时间点的材料化聚合相同的事情。为了做到这一点,我们应该定义一个事件,称为StoryResettedEvent,其效果是通过完全清空来重置聚合的状态,并执行以下步骤:
- 将StoryResettedEvent应用于我们的聚合,以使其状态被清空
- 获取我们正在处理的聚合的前n个事件(从第一个保存的事件到目标状态A的所有事件)
- 将所有事件应用于聚合实例
我认为这种方法的主要问题在于用来清空聚合状态的事件:它似乎有些不太自然,没有实际的业务含义,而是一种实现回滚功能的技巧。
第三种方法:每次保存事件时将补偿事件持久化
我们想出了第三种方法,也是基于补偿事件的概念。基本思想是应用程序的每个事件都可以使用一个包含相应补偿事件的属性进行丰富。
在引发事件的代码点,可以立即计算要引发的事件的补偿事件(基于聚合的当前状态和事件的形状),以便将事件丰富为包含此信息的事件,然后该信息将保存在事件存储中。通过这样做,补偿事件始终可用,随时准备在回滚请求时使用。这种解决方案的缺点是每个领域事件必须被修改,并且只有一小部分我们必须计算并保存在事件存储中的补偿事件将对实际回滚有用(其中大部分将永远不会被使用)。
结论
我认为解决此问题的最佳选择是使用基于状态比较的算法(第一个提出的解决方案),但我们仍在评估要做什么。
有人已经有类似的要求了吗?还有其他实现回滚的方法吗?我们是完全错过了重点并遵循错误的方法来解决问题吗?
感谢您的帮助,任何建议都将不胜感激。