实践DDD的一种思路

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

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

CQRS的迷思

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

EventSourcing

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

最终一致性

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

DDD应用框架

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

整体架构

这个是enode的架构图
image.png

使用约束

  • 一个命令一次只修改一个聚合根
  • 聚合间只能通过领域消息交互
  • 聚合内强一致性
  • 聚合间最终一致性

核心思路

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

首先做这个限制是从业务研发的角度来考虑的,这会让命令的职责更加具体,便于问题的拆解,职责的划分,如果一个命令要修改多个聚合根,应该通过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有着本质的区别。