DDD 领域驱动设计学习(四)- 架构(CQRS/EDA/管道和过滤器)

命令和查询职责分离(CQRS)

做过应用系统的人对系统开发的内容应该有一个感觉,系统后端的主要工作包括CRUD (Create, Read, Update, Delete) 增查改删;业务逻辑和大量查询。大部分的软件研发的架构和方法论主要基于业务逻辑方面,而且通常查询的业务量要远远大于做数据变更的业务量,CRUD和查询如果采用复杂的架构反而感觉有点过重了。
CQRS也符合关注点分离的思想,看到CQRS的逻辑视图,第一感觉跟自己以前做过的一个系统中把所有查询单独做了一个动态查询组件,开发者只需要定义查询,写入对应的SQL语句,这个组件就会完成调用后台服务传输到前台的所有工作,这个组件大大简化了开发查询服务的开发效率。
CQRS架构本身只是一个读写分离的思想,如下图。


DDD 领域驱动设计学习(四)- 架构(CQRS/EDA/管道和过滤器)_第1张图片
CQRS

实现方式多种多样,比如数据存储不分离,仅仅只是代码层面读写分离,这个就是我们以前曾经做过的方式;另外数据存储的读写分离,C端负责数据存储,Q端负责数据查询,Q端的数据通过C端产生的Event来同步,可以看成是读写分离和Event Sourcing的一个结合体。CQRS的原理上看很完美,不过在Martin Fowler的文章中,对CQRS的实现复杂性提出了告警:“ Despite these benefits, you should be very cautious about using CQRS.”
个人的看法,大型应用系统采用读写分离本身就是一种常见的方案。而在某些场景中,使用传统的数据库技术也能够解决同样的问题,并且要简便的多。对于C部分所采用的事件驱动方式,这个跟传统软件开发模式有不少差异。在一些复杂的应用场景中可能有不少问题需要进行重新设计。
Dahan认为在某一种场景下是非常适合应用CQRS架构的,即具有高竞争性的业务领域。在这种领域中的负载非常大,而且具有高度的局域性。

DDD 领域驱动设计学习(四)- 架构(CQRS/EDA/管道和过滤器)_第2张图片
CQRS/ES

事件驱动架构

EDA事件驱动架构首先不是对于传统的面向业务流程,数据等各种架构模式的完全否定,而是解决传统架构下无法很好解决的一些问题。传统模式里面更加关注业务流程和业务对象,而EDA模式下将更加关注在整个业务流程中的关键状态点,已经由关键状态点触发的有明确业务含义的业务事件。

在《实现领域驱动设计》一书中提出采用EDA结合六边形架构的思路。
DDD 领域驱动设计学习(四)- 架构(CQRS/EDA/管道和过滤器)_第3张图片
EDA和六边形架构

一个系统的输出端口所发出的领域事件将被发送到另一个系统的输人端口,此后输人端口的事件订阅方将对事件进行处理。对于不同的BC来说,不同的领域事件具有不同含义。在一个BC处理某个事件时,应用程序 API将采用该事件中的属性值来执行相应的操作。
在一个多任务处理过程中,如果这个事务只有在所有的参与事件都得到处理之后,我们才能认为这个多任务处理过程完成了,某种领域事件只能表示该过程中的一部分。这时候这个过程的处理就需要结合其他架构进行处理。如果处理事件和消息在下面章节做介绍。

管道和过滤器(Pipes And Filters)

管道过滤器和生产流水线类似,在生产流水线上,原材料在流水线上经一道一道的工序,最后形成某种有用的产品。在管道过滤器中,数据经过一个一个的过滤器,最后得到需要的数据。
通过标准化每个组件接收和发射的数据的格式,这些过滤器可以组合在一起成为一个管道。这有助于避免重复代码,并且可以很容易地移除,替换或集成额外的组件。
看一个Linux下的Shell命令,该命令用于在 phone_numbers.txt文件中统计含有电话区号“303"的所有文本行的数量。

 $ cat phone_numbers.txt | grep303 | wc -l  

在以上的命令工具中,每个工具都接收一个数据集,对其进行处理,再输出另一个数据集。输出数据集和输人数据集是不同的,因为每一个命令都充当着过滤器的作用。在整个过滤过程完成之后,输出数据和输人数据可能完全不一样了。在本例中,最原始的输人是一个文本文件,但最终的输出则只有一个数字“3”。

但如何将上面的基本原则用于事件驱动架构呢?如下图所示:
DDD 领域驱动设计学习(四)- 架构(CQRS/EDA/管道和过滤器)_第4张图片
事件驱动

长时间处理过程(Saga)

对上面的管道和过滤器的例子进行扩展,可以得到另一种事件驱动的分布式的并行处理模-----长时处理过程(Long-Running Process)。一个长时处理过程有时也称为Saga。

DDD 领域驱动设计学习(四)- 架构(CQRS/EDA/管道和过滤器)_第5张图片
长时处理过程

和先前的例子不同的是,此时的长时处理过程将由PhoneNumberExecutive(下面简称 PNE)来启动,同时它还将对处理过程进行跟踪。 PNE可以重用 PhoneNumbersPubIisher,也可以不再重用。PNE可以通过应用服务或者命令处理器的形式实现,它将跟踪长时处理过程的各个阶段。同时PNE它还知道一个长时处理过程何时执行完毕,并在这些过程执行完毕之后,再执行其他任务。
然而,这个例子中还存在一个问题。PNE无法知道所接收到的两个领域事件是否来自同一个并行处理过程。处理过程并行启动,完成事件无序地产生,那么 PNE如何知道是哪个处理过程执行完毕了呢?在电话号码统计这个例子中,出现这个问题可能并不严重。但是,当处理真实的企业业务领域时,这样的问题却有可能是灾难性的。
解决这个问题的第一步是在每个领域事件中加人处理过程的身份标识。这个标识可以和引发处理过程的领域事件的标识相同,比如 AllPhoneNumbersListed事件。我们可以使用 UUID。 PNE只有在接收到具有相同标识的领域事件时才会输出日志记录。然而PNE并不需要等待所有事件的到达,它也是一个事件订阅方,在事件到达时将自动启动相应的处理过程。
在实际的领域中,一个长时处理过程的执行器将创建一个新的类似聚合的状态对象来跟踪事件的完成情况。该状态对象在处理过程开始时创建,它将与所有的领域事件共享一个唯一标识。长时处理过程的状态对象下图所示。
DDD 领域驱动设计学习(四)- 架构(CQRS/EDA/管道和过滤器)_第6张图片
状态对象

当并行处理的每个执行流运行完毕时,执行器都会接收到相应的完成事件。然后,执行器根据事件中的过程标识获取到与该过程相对应的状态跟踪对象实例,再在这个对象实例中修改该执行流所对应的属性值。
当与遗留系统的集成存在很大的时间延迟时,采用长时处理过程将非常有用。即便时间延迟和遗留系统并不是我们的主要关注点,我们依然能从长时处理过程中得到好处,即由分布式和并行处理所带来的优雅性。这样也有助于我们开发高可伸缩性、高可用性的业务系统。

事件源

一个对象从创建开始到消亡会经历很多事件,以前我们是在每次对象参与完一个业务动作后把对象的最新状态持久化保存到数据库中,也就是说我们的数据库中的数据是反映了对象的当前最新的状态。而事件溯源则相反,不是保存对象的最新状态,而是保存这个对象所经历的每个事件,所有的由对象产生的事件会按照时间先后顺序有序的存放在数据库中。
DDD 领域驱动设计学习(四)- 架构(CQRS/EDA/管道和过滤器)_第7张图片
Event Sourcing

那么,事件到底如何影响一个领域对象的状态的呢?Event sourcing事件溯源是借鉴数据库事务日志的一种数据持久方式,在事务日志中记录导致状态变化的一系列领域事件。通过持久化记录改变状态的事件,通过重新播放获得状态改变的历史。 事件回放可以返回系统到任何状态,这个过程就是所谓的事件溯源。
另一方面,由于事件流本身具有逻辑上严格次序性,因此使用统一的事件流(事务日志)能够很自然实现事务机制,无需额外ACID机制或2PC之类同步强硬方式。
我们可以看到基于这样的设计,领域对象的状态完全是由事件驱动的。不仅如此,事件还可以被事件总线分发出去,通知领域模型外的一切事件响应者发生了什么,基于这种Publish-Subscribe的通信模式,我们可以最大限度的实现系统的松耦合。

参考资料
1. 深度长文:我对CQRS/EventSourcing架构的思考
2. DDD CQRS架构和传统架构的优缺点比较
3. 对CQRS的一次批判性思考
4. 再谈EDA事件驱动架构
5. Reference 6: A Saga on Sagas

你可能感兴趣的:(DDD 领域驱动设计学习(四)- 架构(CQRS/EDA/管道和过滤器))