我一直在阅读关于CQRS+EventSourcing
模式的内容(我希望在不久的将来应用它),我发现所有幻灯片和演示文稿都有一个共同点,那就是为了恢复模型状态而拍摄快照,但是没有分享这样做的模式/策略。
我想知道您在这方面的想法和经验,特别是涉及以下方面:
- 何时拍摄快照
- 如何建模快照存储
- 应用程序/缓存冷启动
TL;DR: 在你的CQRS+EventSourcing
应用程序中如何实现Snapshotting
?优缺点是什么?
我一直在阅读关于CQRS+EventSourcing
模式的内容(我希望在不久的将来应用它),我发现所有幻灯片和演示文稿都有一个共同点,那就是为了恢复模型状态而拍摄快照,但是没有分享这样做的模式/策略。
我想知道您在这方面的想法和经验,特别是涉及以下方面:
TL;DR: 在你的CQRS+EventSourcing
应用程序中如何实现Snapshotting
?优缺点是什么?
对事件溯源模型进行快照是一种性能优化。性能优化的第一条规则是什么?不要这样做。
具体来说,快照会减少从事件存储库重新加载模型历史时损失的时间。
如果您的存储库可以保留模型在内存中,则不太需要频繁重新加载。因此,快照带来的优势将很小。因此:不要这样做。
如果您可以将模型分解为聚合,也就是说,您可以将模型的历史分解为许多实体,这些实体具有不重叠的历史记录,则您的一个模型长历史记录将变成许多简短的历史记录,每个记录都描述了单个实体的更改。需要加载的每个实体历史记录都将非常短,因此快照带来的收益将很小。因此:不要这样做。
我今天正在处理的系统需要高性能但不需要全天候可用性。因此,在关闭系统进行维护并重新启动它时,我必须加载和重新处理所有我的事件存储,因为我的全新系统不知道要处理哪些聚合标识符。我需要更好的起点来使我的系统重新启动更有效。
您担心在存储库内存缓存冷却时错过写入SLA,并且您有具有许多事件需要重新加载的长模型历史记录。与尝试将模型历史记录重构为较小的流相比,添加快照可能更为合理。好的……
快照存储是一个“读模型”--在任何时间点,您都应该能够从事件存储中持久化的历史记录中删除模型并重新构建它。
从存储库的角度来看,快照存储是缓存;如果没有可用的快照,或者存储本身无法在SLA内响应,则希望返回重新处理整个事件历史记录,从初始种子状态开始。
服务提供商接口将看起来像:
interface SnapshotClient {
SnapshotRecord getSnapshot(Identifier id)
}
SnapshotRecord将向存储库提供需要消耗快照的信息,这将包括至少:
然后,该模型将从Memento中重新构建快照状态,从事件存储库加载历史记录,并向后扫描(即从最近的事件开始),查找在SnapshotRecord中记录的事件,然后按顺序应用后续事件。
SnapshotRepository本身可以是键值存储(每个给定ID最多一个记录),但具有Blob支持的关系数据库也可以很好地工作。
select *
from snapshots s
where id = ?
order by s.total_events desc
limit 1
快照投影仪和存储库是紧密耦合的——它们需要就实体的状态在所有可能的历史记录上达成一致,它们需要就如何脱水/重建动态的实体达成一致,并且它们需要就将用于定位快照的ID达成一致。
紧密耦合也意味着你不需要特别担心memento的模式;一个字节数组就可以了。
但是,它们不需要与自己之前的版本达成一致。快照投影仪2.0会丢弃/忽略快照投影仪1.0留下的任何快照——毕竟快照存储只是一个缓存。
我正在设计一个可能每天会生成数百万事件的应用程序。如果我们需要在6个月后重建视图,我们该怎么办?
这里有一个更有说服力的答案就是要显式地对时间进行建模。你是否有一个生命周期为6个月的实体,还是有180多个每天都存在的实体?会计是一个好的参考领域:在财政年度结束时,账目被关闭,然后带余额的账目开启进入下一年度。
Yves Reynhout经常谈论时间建模和调度;Evolving a Model可能是一个很好的起点。
有一些情况你需要确保进行快照。但是有几个例子,一个常见的例子是账户在分类帐中。你可能会有数千甚至数百万的信用/借记事件产生账户的最终BALANCE
状态 - 不进行定期快照是不明智的。
我在设计Aggregates.NET时对快照的方法是默认关闭,要启用聚合或实体必须继承AggregateWithMemento
或EntityWithMemento
,然后你的实体必须定义一个RestoreSnapshot
、一个TakeSnapshot
和一个ShouldTakeSnapshot
是否进行快照的决策留给实体本身。一个常见的模式是
Boolean ShouldTakeSnapshot() {
return this.Version % 50 == 0;
}
当然,这需要每50个事件拍摄一次快照。
在读取实体流时,我们首先检查快照,然后从拍摄快照的那一刻开始读取实体的其余流。也就是说:不要请求整个流,只请求我们没有拍摄过的部分。
至于存储-你可以使用任何东西。VOU是正确的,因为键值存储最好,因为您只需要1.检查是否存在2.加载整个内容-这对于kv非常理想
对于系统重新启动-我真的不明白您描述的问题是什么。没有理由让您的域服务器具有状态性,即在不同时间点执行不同的操作。它应该只做一件事- 处理下一个命令。在处理命令的过程中,它从事件存储加载数据,包括快照,对实体运行命令,这将产生业务异常或领域事件,这些事件被记录到存储中。
我认为您可能正在尝试通过缓存和冷启动来进行过度优化。