在微服务架构盛行的当下,DDD(领域驱动设计)也得到了崭新的发展。同时,随着DDD的不断发展,也诞生了一些新的设计思想和开发模式,今天要介绍的事件溯源是其中具有代表性的一种模式。
事件溯源模式是DDD领域中一种新的架构模式,专门用来处理应用程序中的状态变化。事件溯源模式的实现方式与传统的应用程序开发方法有很大不同,表现在对应用程序状态的存储和检索过程。
事件溯源模式设计理念
在对事件溯源进行详细展开之前,我们先来回顾一下传统应用程序中关于“状态”的维护方法。让我们先从一个典型的应用场景开发说起,在日常开发过程中,开发人员经常需要处理与用户相关的操作,例如用户的创建、密码修改、地址更新、权限变更等。
在这个场景中,代表用户概念的User就是核心的领域对象。那么,基于该领域对象,你会怎么处理对应的业务操作呢?
域溯源模式
在传统应用程序中,我们使用常见的数据库(如关系数据库、NoSQL数据库)来创建、修改或查询业务领域对象的状态。另一方面,我们通过把这些状态持久化到一定的数据存储媒介之后,也可以通过领域事件的形式把事件发布到消息中间件。基于以上分析,我们认为这种状态变化的过程是领域对象来驱动的,也就是说只有领域对象主动更新自己的状态并生成领域事件,我们才能获取到应用程序状态的变化。
反过来讲,如果领域对象没有主动更新数据或生成任何领域事件,那么我们也就无法感知到应用程序的状态已经发生了变化。从状态的源头(Source)来讲,我们认为这种处理方式是一种域溯源(Domain Sourcing)的方式。
基于域溯源模式,我们在对领域对象执行一定操作之后会把对象的最新状态持久化到数据库中,也就是说数据库中的数据反映了对象当前最新的状态。
从上图中,我们可以看到针对用户状态更新的各个操作,在数据库中只会存在一条数据记录,反应了该用户的当前的最新信息。至于各个操作的具体执行过程,通常可能会设计一张操作日志表。
显然,域溯源的实现过程比较简单,也符合开发人员的直观认知,因为它们使用了存储和查询应用程序状态的传统实现方案。只要能够正确保存领域对象,我们就能获取应用程序最新的状态信息。
事件溯源模式
接下来,我们要引入的事件溯源机制则采用了另一种设计理念。与域溯源模式不同,事件溯源模式只关注于处理领域对象上发生的领域事件。也就是说,事件溯源模式不是保存对象的最新状态,而是保存这个对象所经历的每个事件,所有由对象产生的事件会按照时间先后顺序有序的存放在数据库中。
当我们需要这个对象的最新状态时,只要先创建一个空的对象,然后把和该对象相关的所有事件按照发生的先后顺序从先到后全部执行一遍。这个过程就是事件溯源,如下图所示。
从上图中可以看到,事件溯源的核心设计思想在于:不保存对象的最新状态,而是保存导致对象状态发生变化的所有事件,这样就可以通过对这些事件进行溯源得到对象的最新状态。
显然,基于事件溯源的设计思想,一个事件就是表示一个事实,事实是不能被磨灭或修改的,所以事件本身是不可修改的(Immutable),我们只能执行新增和查询操作。而对比域溯源模式和事件溯源模式,我们不难发现有两个差异点。
以上两点构成了事件溯源模式的能够得以实施的前置条件。当我们把应用程序的状态变更全部进行持久化之后,接下来我们就可以真正实现所谓的溯源操作了。
实现事件溯源模式
理解了事件溯源的基本概念之后,接下来我们讨论具体的实现过程。
事件存储
实现事件溯源的第一步是确保领域事件都得到持久化。在事件溯源模式中,存储事件的组件被称为事件存储器(Event Store)。下图展示了采用事件溯源机制下各组件的交互示意图。
结合上图,我们来举一个具体的示例。如果我们正在设计一个典型的用户应用程序,那么基于上图中的交互过程,在用户更新密码时,就会生成一个PasswordUpdatedEvent这样一个领域事件。我们只需要对这个领域事件进行持久化,而不需要保存User这个领域对象。PasswordUpdatedEvent事件将被持久化到一个专门构建的事件存储器UserEventStore中。
事件回放
接下来,假设需要获取用户应用程序中User这个领域对象的最新状态,那么基于事件溯源机制,我们将采用一种比较特殊的实现方式。首先,我们会从事件存储器中加载User对象上已经发生的所有领域事件。然后,我们在User对象上依次执行所有领域事件所包含的状态变化信息,从而确保User对象达到当前的最新状态。整个执行过程如下图所示。
有时候,我们把在User对象上重新执行领域事件的这个过程成称为事件回放(Event Replay)。可以看到,基于事件溯源机制,我们采用的是一种纯事件驱动的实现方法。
整合DDD和事件溯源
在DDD中,我们把某一个独立的业务模块划分成一个限界上下文。在每个限界上下文中势必会存在聚合对象。生成领域事件的一般就是这些聚合对象,而聚合的创建常见是在应用服务中。因此,DDD和事件溯源机制的整合效果如下图所示。
另一方面,领域事件一般都具有传播性,如果我们想要把领域事件传播出去,那么可以引入一个事件路由器来实现这一目标。
结合前面介绍的案例场景,那么User就是聚合对象,而我们可以把对应的事件路由器命名为UserEventRouter。PasswordUpdatedEvent这个领域事件被我们发送到事先已经设计好的消息路由通道,从而供其他限界上下文中的事件处理器进行消费。这个过程中,领域事件是否被持久化实际上是没有任何约束的。
在应用程序开发过程中,我们通常使用关系型数据库来维护业务对象的最新状态。这是一种比较传统的实现方案,但如果我们想要进一步明确该业务对象所执行的各种操作,问题就变得没有那么简单。而今天介绍的事件溯源机制为我们提供了另一种完全不同的实现思路。在事件溯源机制中,我们保存的不是数据状态本身,而是引起这些状态发生的各种事件。通过在业务对象上依次执行这些事件,我们就能够获取该业务对象所经历的各种操作过程和结果。在日常开发过程中,针对如何实现事件溯源,我们需要考虑事件的存储、事件的回放等技术组件,同时也需要考虑DDD和事件溯源的整合过程,因为时间溯源的主要应用方式就是在DDD应用程序中。