【发布时间】:2018-09-05 02:50:21
【问题描述】:
我研究 DDD 已经有一段时间了,偶然发现了 CQRS 和事件溯源 (ES) 等设计模式。这些模式可用于帮助以更少的努力实现 DDD 的某些概念。
然后我开始开发一个简单的软件来实现所有这些概念。并开始想象可能的失败路径。
为了澄清我的架构,下图描述了一个来自前端并到达控制器的请求,我是后端(为简单起见,我忽略了所有过滤器、绑定器)。
- 演员发送一份表格,其中包含他想从一个帐户中提取的金额。
- 控制器将视图模型传递到应用层,在应用层中将其转换为一个命令
- 应用层打开一个工作单元 (UOW) 将 VM 映射到命令并将命令发送到调度程序。
- 调度程序找到知道如何处理命令(帐户)的相应聚合类,并向工厂请求帐户的特定实例。
- 工厂创建一个新的帐户实例并从事件存储中请求所有事件。
- 事件存储返回帐户的所有事件。
- 工厂将所有事件发送到聚合,以便其内部状态正确。并返回帐户的实例。
- 调度程序将命令发送到帐户,以便对其进行处理。
- 账户检查是否有足够的钱进行提款。如果有,它会发送一个新事件“MoneyWithdrawnEvent”。
- 此事件由更改其内部状态的聚合(帐户)处理。
- 应用程序层关闭 UOW,当它关闭时,UOW 检查所有加载的聚合以检查它们是否有新事件要保存到事件存储中。如果有,它将事件发送到存储库。
- 存储库将事件保存到事件存储中。
可以添加很多层,例如:聚合的缓存、事件的缓存、快照等。
有时 ES 可以与关系数据库并行使用。因此,当 UOW 保存已发生的新事件时,它还将聚合保存到关系数据库中。
ES 的一个好处是它有一个中心数据源,即事件存储。因此,即使内存甚至关系数据库中的模型损坏,我们也可以从事件中重建模型。
有了这个事实来源,我们可以构建其他系统,以不同的方式使用事件来形成不同的模型。
但是,要实现这一点,我们需要真理的来源是干净的,没有被破坏的。否则所有这些好处都不会存在。
也就是说,如果我们在图中描述的架构中考虑并发性,可能会出现一些问题:
- 如果actor在一个排序周期内向后端发送了两次表单,后端启动了两个线程(每个请求一个),那么他们会调用两次应用层,并启动两个UOW,以此类推.这可能会导致两个事件存储在事件存储中。
这个问题可以在很多不同的地方处理:
前端可以控制哪些用户/参与者可以执行什么操作以及执行多少次。
Dispatcher 可以拥有所有正在处理的命令的缓存,如果存在引用同一聚合(帐户)的命令,则会引发异常。
存储库可以创建一个新的聚合实例,并在保存之前运行事件存储中的所有事件,以检查版本是否仍与第 7 步中获取的版本相同。
每个解决方案的问题:
-
前端
- 用户可以通过编辑一些 javascript 来绕过这个限制。
- 如果打开了多个会话(例如,不同的浏览器),则需要一些静态字段来保存对所有打开的会话的引用。并且有必要锁定一些静态变量才能访问该字段。
- 如果有多个服务器用于执行特定操作(水平缩放),则此静态字段将不起作用,因为有必要在所有服务器之间共享此字段。因此,需要一些层(例如 Redis)。
-
命令缓存
要使此解决方案起作用,有必要在读取和写入命令缓存时锁定命令缓存的某些静态变量。
如果有多个服务器用于执行应用层的特定用例(水平扩展),则此静态缓存将不起作用,因为有必要在所有服务器之间共享此缓存。因此,需要一些层(例如 Redis)。
-
存储库版本检查
要使该解决方案有效,有必要在检查(数据库版本等于步骤 7 中获取的版本)并保存之前锁定一些静态变量。
如果系统是分布式的(水平比例),则有必要锁定事件存储。因为,否则,两个进程都可以通过检查(数据库的版本等于步骤 7 中获取的版本),然后一个保存,然后另一个保存。并且根据技术,不可能锁定事件存储。因此,将有另一个层来序列化对事件存储的每次访问并添加锁定存储的可能性。
这种锁定静态变量的解决方案有点好,因为它们是局部变量并且非常快。但是,依赖于 Redis 之类的东西会增加一些较大的延迟。如果我们谈论锁定对数据库的访问(事件存储),甚至更多。甚至更多,如果这必须通过其他服务来完成。
我想知道是否有任何其他可能的解决方案来处理这个问题,因为这是一个主要问题(事件存储损坏),如果没有办法解决它,整个概念似乎是有缺陷的。
我对架构中的任何更改持开放态度。例如,如果一种解决方案是添加一个事件总线,以便所有内容都通过它汇集,那很好,但我看不出这能解决问题。
我不熟悉的另一点是卡夫卡。不知道 Kafka 有没有针对这个问题提供的解决方案。
【问题讨论】:
-
无关:您可能需要更仔细地考虑您的界限在哪里。您的
domain model应该知道如何获取事件历史记录和withdraw command并创建新事件。那是你的功能核心——所有的管道都应该在它之外。 -
您可能还需要考虑是否需要步骤 1 + 2。为什么不直接创建命令而不是视图模型并派生命令。这将减少您为获得相同的可测试结果而需要编写的代码的数量和复杂性。
-
嗨@VoiceOfUnreason,在图片中描述的架构中,Aggregate 是处理命令和事件(以及事件历史)的类。并且聚合在域层中。
-
嗨@Codescribler,我可以删除应用程序层,以便控制器负责将视图模型映射到命令中。但是,我更喜欢包含它,因此可以更轻松地测试代码。控制器变得更轻(更少的代码)。
-
我的意思是,为什么要映射任何东西?这是额外的复杂性,没有真正的收获。
标签: architecture domain-driven-design cqrs event-sourcing dddd