当前位置: 首页 > 后端技术 > Java

DDD在订单系统中的实践(二)——DDD与CQRS结合

时间:2023-04-01 15:56:55 Java

DDD与CQRS结合背景知识在讨论这个问题之前,我们先来回顾一下几个基本概念。聚合根:按照不变规则设计的聚合边界,聚合内的所有操作都严格遵守业务规定,具有很强的一致性保证。聚合根的概念比较抽象,但却是领域建模核心的核心。这里我不讨论域的划分,也不讨论如何建立聚合根,只谈一下我对“聚合边界设计以符合不变规则”这句话的理解。首先,我们必须认识到,领域建模本质上是一种知识建模解决方案。在这个前提下,我们的建模一定不能违反业务规则。聚合本质上是“业务规则”在系统中的一致体现。比如在“订单拆解”的上下文中,不仅父订单的状态变为拆解状态,子订单的生成也一样。在此基础上,另一个与聚合根相关的设计原则“通过聚合根访问实体”也就不难理解了。通过聚合根,可以有效限制实体(变量对象)在系统中的访问,使得对实体的所有修改都在“不变规则”下进行,可以避免“业务规则”被篡改被毁。洋葱模型有两层同心圆:外层代表通信机制和基础设施;内层代表业务逻辑理解了聚合根之后再看“洋葱模型”或者“六边形架构”就不难理解了。聚合根设计的根本出发点是“业务原则的高内聚”。从这个角度来看,系统中的数据反转必须受到领域模型的约束。流程可以简单概括为用户层(同步事件处理(即外部API),异步事件处理(消息监听))==>领域层(业务逻辑)==>基础设施层(聚合中的数据变化持久化(DB或其他存储形式),跨系统持久化变化(发送Domain事件))CQRS:命令查询职责分离,读写双模型;命令模型用于高效执行写入/更新操作,而查询模型用于有效支持各种读取模式。通过域事件或各种其他机制将命令模型中的更改传播到查询模型,以保持两个模型之间的数据同步。CQRS实现了两种策略同步方案,实时双写,优点是实时性高,双写带来额外的写入时间开销。具体案例请参考《拍卖系统优化历程(一) -- 建立拍品Lot模型,实践CQRS》;异步方案使用消息或其他异步同步机制,让两个模型实现最终一致性(这里介绍我们如何使用数据库的读写同步策略实现读写模型的最终一致性);支付流程的域建模和CQRS登陆顺序域划分顺序基本域划分安全构造实体(不变性规则的一部分)围绕顺序我们拆分成多个域(“顺序”作为各自域的聚合根),尽管这些不同域中的订单基于相同的底层数据,但“订单”在不同上下文中的定义并不相同。以“PayingOrder”和“SplittingOrder”为例,底层数据本质上都是基于同一个数据,这两个聚合的边界一方面是他们领域的重点。商业问题是不同的。另一方面,从“秩序”实体的角度来看,它的生命状态有着明显的界限。PayingOrder是待支付的订单,SplittingOrder是已经支付完成的待拆分订单。订单是否支付,这两个订单实体有着本质的区别。区别。注意,这里我们讨论的是实体状态,而不是数据状态,这一点非常重要。在此前提下,如果订单状态不满足当前聚合下的状态要求,则聚合中的实体构建将失败。通过屏蔽非安全实体(或聚合)构造来避免不安全因素的影响。问题不止于此。我们之前定义的不安全感,有时可能是某个时间段内的误判。比如由于限流降级、master同步延时等策略的有效性,我们基于有偏数据得到了“不安全访问”的错误判断(这也是我们做读写的头疼问题分离),那么如何解决呢?这是在聚合之外的最终一致性策略的帮助下实现的。聚合外部的最终一致性我们需要在聚合内部追求强一致性保证。在聚合之外,我们经常使用最终一致性来解耦系统(上图中的黑色箭头)。对于聚合中产生的事件,我们可以使用Kafka,例如消息中间件进行通信。必须能够重试上述误判(如消息重试)直到最终一致,而不需要投入过多的精力去干预业务实现,因为系统间的一致性问题没有master同步延时,限流和限流的影响退化也存在。洋葱模型在支付事件的处理上比较抽象。这里具体实现的另一种描述方式可以参考我之前写的两篇文章《DDD实践落地(二)》《支付订单领域建模实践》从CommandModel到QueryModel我提到了两种实现CQRS的机制,不难理解。同步方案中,DomainRepository访问读写模型各自的数据源,进行同步数据更新;异步方案中,CommandModel(领域实体)执行命令后发送响应领域事件,通过订阅领域事件更新查询模型;借助数据层的读写同步机制,实现读写模型的最终一致性;CQRS中读写职责的分离,本质上是通过实体职责的分离,简化了读模型(优化查询)的构建,并且由于读模型不再承担写命令的执行,因此也避免了当数据不完整或不实时时,读取模型不会进一步传播过期读取。在“订单支付”和“订单发货”场景中,我们采用了不同于上述两种读写模型方式的同步方案(或异步方案的变体)。外部系统收到支付完成事件后,通过read模型查询要拆分的订单。待拆分订单的有效构建是基于待支付订单支付行为的一致性处理(PayingOrder负责写入模型对应数据的强一致性,SplittingOrder只需要验证其状态能够保证数据已经完全同步),如果构建成功,说明读取的模型数据已经同步完成并返回数据,否则已经完成支付的订阅者会继续重试,等待读模型数据同步完成。具体实现细节参见订单系统中CQRS实践(一)订单读写库的使用细节。读模型进行事件重放,实现逻辑一致性;在读写同步方案中,我们可以看到从同步到领域事件,影响构成读写模型的数据层向上反馈,实现最终一致性。那么如果在某些场景下,阅读模型不通过数据层反馈事件响应,而是直接作用于逻辑层,是否还可行呢?具体实现细节参见订单系统CQRS实践(一)中“订单查询流程”的描述。为了使该方案生效,必须确定写入模型的事件可以在读取模型的构建期间重放。在很多场景下,这个条件还是很难达到的。总结回过头来,让我们看看整个计划。我们并没有刻意在业务实现上做一些调整,但是我们还是能够完成CQRS的实现,而这一切的根源其实就是一开始讨论的聚合根原理:“Modelingtrulyinvariantconditionswithinaggregate边界”,“在聚合边界之外使用最终一致性”。从书本到实践,这也是我最近实践的一个总结,希望对大家有所启发。