写点什么

实践 DDD 的一种思路

作者:安然
  • 2021 年 12 月 22 日
  • 本文字数:2144 字

    阅读完需:约 7 分钟

在实践 DDD 的道路上,我们一开始容易被各种概念带偏,写出各种样板式代码,满足相应的分层。定义聚合、实体、值对象,然后对聚合操作要有Repository,当聚合比较复杂时还要有Factory,后面又冒出来一个领域服务的概念,这个是大家最容易迷惑的,领域服务不是领域内的服务,不是对领域能力包装成的一个服务,领域服务是实在不知道放到哪个聚合下才妥协的产物,领域服务能不用最好就不用,因为它本来是不该存在的。同时传统 DDD 在实践过程中增加的读写放大,多次操作数据库怎么优化,没有给出很好的解法。


综上:我觉得这也是大家实践起来一个很大的门槛

CQRS 的迷思

近些年,对我影响最大的一个概念就是 CQRS,在各种业务场景的设计中,或多或少都会有类似的设计,相比传统的 CRUD,CQ 是一种思维,在遇到业务场景时,会主动的识别 Command 和 Query,因为两者面临要解决的问题域不尽相同,现有的存储体系,读和写几乎是不可兼得的,所以我们会有异构数据源,针对读库的个性化定制又特别多,写模型又要保障很好的性能,稳定性和扩展性。CQRS 的思路我觉得是现有体系下比较合理的一个解

EventSourcing

事件溯源,比较难落地的一个概念,短平快的互联网应用,要记录每次操作的事件,无疑是一种负担

最终一致性

当我们采取了传统 DDD 的架构思路,为了写出符合 DDD 的代码,在复杂技术的实现上还要考虑到会不会兼容这种写法,为了实现聚合间的最终一致,我们的分布式事务如何做

DDD 应用框架

enode 提供了一个思路解决上述问题,和 axon 的解决思路还不太一样

整体架构

这个是 enode 的架构图


使用约束

  • 一个命令一次只修改一个聚合根

  • 聚合间只能通过领域消息交互

  • 聚合内强一致性

  • 聚合间最终一致性

核心思路

一个命令一次只修改一个聚合根


首先做这个限制是从业务研发的角度来考虑的,这会让命令的职责更加具体,便于问题的拆解,职责的划分,如果一个命令要修改多个聚合根,应该通过 Saga(Process Manage)来完成


加上这个约定后带来的收益:


  • 同一个聚合根的命令操作都会路由到同一个分区,聚合根就可以常驻内存(In-Memory),这样就不必每次重建聚合根,缓存利用率聚合是 100%,是一种大限度利用内存的设计

  • 命令路由到同一个分区,命令的操作顺序就可以保障(命令会携带聚合根的版本),这就保障了聚合根在同一时刻只有一个在操作,直接避免了并发问题,因为在设计上是无锁的

  • 关于命令操作顺序的保障,为了提升吞吐,要求队列是无序消费,但队列无序了怎么保证操作是有序的呢,这点就有点类似 Flink 中的 watermarker 的设计了,聚合根的 mailbox 会记录每个消息的版本,如果高版本的数据先到,数据就会暂存,等到中间的版本处理完成才处理,通过 mailbox 中的顺序保证了操作的有序

基建依赖

分布式消息队列

依赖队列的原因主要有三点:


  1. 面向不同服务场景资源隔离,可针对性的优化

  2. 为了 C 端高吞吐,可通过队列无限扩缩容,且节省资源

  3. 为了同一个聚合根路由到同一个消费者,减少聚合的重建,缓存利用率高

EventStore

在存储方面需要额外保障两张表,这个也是不得已而为之的一个设计,因为要实现 EventSourcing,事件记录总要有个地方放事件表(event_stream),聚合根消费进度表(published_version)限定要提供的能力:


  1. 批量提交事件,同时能识别出哪些是重复命令和重复的版本

  2. 通过聚合根 id 和 commandId 点查

  3. 通过聚合根 id 和 version 点查

  4. 通过聚合根 id 和最小最大 version 范围查找


同时留了扩展,可自主的选择实现,目前提供默认的实现(MySQL,PG,MongoDB)

编程模型

事件驱动的迷思:


  • 什么时候采取事件驱动,什么时候使用过程式编程呢?

  • 命令和事件的区别,两者都是消息,为什么要分开表示呢?


我的理解如下,


命令可以被拒绝。事件已经发生。


这可能是最重要的原因。在事件驱动的体系结构中,毫无疑问,引发的事件代表了已发生的事情。


现在,因为命令是我们想要发生的事情,并且事件已经发生了,所以当我们命名这些事情时,我们应该使用不同的词,命令一般是名词,事件一般是过去分词


举个例子,拿订单系统来说,我们有个外部支付系统的依赖。


当用户在支付系统完成支付后,支付系统会向订单系统发送一个 Command,MarkOrderAsPayed(标记订单已支付),订单在处理这个 Command 时,获取当前订单,调用订单的标记已支付(行为),产生了OrderPayed(订单已支付)事件。


我们可以看到,命令通常由系统外调用,事件是由处理程序和系统中的其他代码提供的。


这是他们分开表示的另一个原因。概念清晰度。


命令和事件都是消息。但它们实际上是独立的概念,应该明确地对概念进行建模。


这两者我理解都是符合人类思维的,首先是基于大脑接收到感知到的消息(Event)产生一个想法【意图】(Command),然后如何实现这个想法,思考的维度是过程式的,在实现的过程中,会产生一些事件消息,这个消息又会影响到大脑。如此循环往复。

事件风暴

简单说下事件风暴的一些经验


  • 按照用例维度开始分析

  • 每个用例以终为始

  • 先枚举主流程,然后补充异常处理,MECE,逻辑闭环,穷尽

  • 补充 actor 和命令信息

  • 最后补充 Policy

Saga 实现

Saga 的实现有两种模式,一种是控制,一种是编排,通过事件消息,完全不需要业务关心技术实现的过程,通过和业务专家沟通需求,事件风暴,画出命令事件,逻辑是不是立马就清晰了。通过打点也可以把可观测性做的很好,同时分布式的事务模型,Saga 在性能方面更有优势,这里和 axon 的 unit of work 有着本质的区别。

用户头像

安然

关注

还未添加个人签名 2018.04.24 加入

还未添加个人简介

评论

发布
暂无评论
实践DDD的一种思路