领域服务、领域事件 领域驱动设计分布式消息转发存储的架构领域事件最终一致性
综合前两篇总结,这篇对领域服务和领域事件做一个梳理。先注明一下,领域服务和应用服务。SOA服务,或者应用间的RPC调用,Restful接口,或者通过消息中间件进行系统间的交互的,都可以归类为应用服务。相较之下,领域服务不一定涉及到远程调用或者重量级事务操作。所以上下文集成也就涉及到,怎样的方式去划分限界上下文,怎么样设计才能尽量减少应用服务的耦合,以及应用服务对于本地模型的防腐。
领域服务
概念
借用《Implementing Domain-driven Design》里面的对于领域服务的定义。领域的某个操作过程或转换过程不是实体或值对象的职责时,应该将操作放在一个单独的接口中,即领域服务,并且要和通用语言保持一致。这里的非实体或值对象操作会有很多种情况,比如某个操作需要对多个领域对象操作,输出一个值对象。在分层的架构中,有点类似于Manager。但是如果过渡抽象manager就会出现贫血,所以还需要确保领域服务是无状态的,并且做好和贫血模型的权衡。可能大多数情况,领域服务的参数都是比实际的领域模型小的,只有些关键属性的值对象。如果服务只操作领域的实体或值对象,则可以考虑下放到domain model中操作。
前面提到了Manager,但是很多应用中都会把Manager抽象成接口的形式,但大多数情况其实完全没有必要,可以通过服务Factory的方式解耦,或者用Spring的@Service注解来注入真正的服务实现类。对于一些简单的领域操作,还可以抽象一个迷你层,这个迷你层也可以称作是领域服务,只不过是无状态,无事务,安全的一个抽象层。
领域事件其实也可以归纳为领域服务,不过领域服务的事件是幂等的。因为领域服务是无事务的,所以事件也是无副作用的,这样在处理聚合依赖的时候,需要保证他们的最终一致性。
领域事件
将领域中发生的活动建模成一系列的离散事件,每个事件都用领域对象来表示。简而言之,领域事件就是领域中发生的事件。还拿租书为例,一本书被借走了,那么需要产生一个借书订单,并且对于租书者来说,需要能查看自己租书的列表和书籍详情,同时这本书也需要被标记为不能再借出的状态(因为已经被借走了)。这里面bookRent就可以作为一个领域事件来发出。
事件的聚合
对于上述的事件模型,我们可以创建具有聚合特性的领域事件。这里我们可以把这个事件本身建模成一个聚合(BookRentEvent 对象),并且有自己的持久化方式。唯一标识可以由一组属性决定,在客户方(Client)调用领域服务的时候创建这个领域事件{new bookRentEvent())},并添加到资源库中,然后再通过消息的方式进行发布。发布成功后再回调更新时间状态。但这里需要注意,消息发布最好和事件资源库在相同的上下文,或共享数据源,这样就可以保证事件的成功提交,在不同上下文系统,就需要做全局事务来保证。而唯一标识在这里的作用就是为了防止消息重发或者重复处理。所以订阅方需要检查重复消息,并且忽略。如果是本地上下文的事件,最好提供equals和hashcode 实现。
结合刚才的例子,在书籍管理上下文中,书被借走了,那么书籍唯一表示和书的状态(Rent被借出)就可以标识一个事件。这个事件中需要有借书人的信息(如id,nick等),那么在持久化这个事件后,可以post一个Eventbus的本地消息,由用户书籍领域服务监听,更新用户书籍列表等一系列操作。然后再Callback到事件源,更新事件状态,处理成功。如果需要处理事件都在本地上下文,处理起来并不麻烦。
发布领域事件
领域事件的发布可以用Observer模式。在本地上下文,也要尽量减少对基础设施或者消息中间件暴露领域模型,所以,需要将本地模型(领域模型)封装成事件的聚合。比如我们不能直接发布一个BookRent聚合的事件,而是一个BookRentEvent,这个Event对象,还会持有一些事件特有的属性,比如可能根据需要,会有occurTime(发生时间),isConsumed(是否已经被处理)。事件发布时,所有订阅方都会同步收到通知。领域事件的主要组件就是publisher和subscriber了。
发送者
发送者本身并不表达一种领域概念,而是作为一种服务的形态。无论用什么技术方式实现,用什么框架,处理事件发送的思路也都可能不尽相同。比如,在web应用中,可以在启动应用的时候处理订阅者向发送者的事件注册(避免注册和处理发送的线程同步问题)。比如可以将关注的事件registe到本地的一个ThreadLocal的publisher List中。应用启动完成后,开始处理领域事件的时候,就可以发送一个事件的聚合。这个事件的聚合是一个事件对象,而不是领域模型中的实体,因为我们要暴露需要暴露的事件给其他上下文,而不是暴露完整的领域对象。如果使用EventBus,我们可以在post的时候,封装一个事件作为参数。
订阅者
事件的订阅者可以作为应用服务的一个独立的组件。因为应用服务是在领域逻辑的外层,如果是纯粹的事件驱动,那么订阅者作为一种应用服务,也可以定位成具有单一职责的,负责事件存储的应用服务组件。
分布式领域事件
在处理分布式事件中,最重要也是最难处理的就是一致性。消息的延迟,处理的不幂等就会影响领域模型状态的准确性和事件的处理。但是我们在系统间交互的过程中,可以用一些技术方式来达到最终一致性。这其中可能就需要进行事件模型的持久化。处理方式可以
1. 领域模型和消息设施共享持久存储的数据源。这种需要事件作为一种本地事件模型存储在和本地领域模型的同一个数据库中。这样保证了本地事务的一致,性能较好,但是不能和其他上下文共享持久化存储。
2. 全局XA事务(两阶段提交)来控制。模型和消息的持久化可以分开,但是全局事务性能差,成本高。
3. 在领域模型的持久化存储中,单独一块存储区域(单独一张事件表)来存储领域事件。也就是做本地的EventStore。但是需要有一个发布事件的消息机制,消息事件是完全私有的。消息的发送可以交给消息中间件来处理。如果可以的话,还可以将时间存储作为Rest资源。事件就可以以一种存档日志的形式对外发布事件(消息队列,通过消息设施或者中间件发送RabitMQ,MetaQ等)。这样还保证了时间的可追溯性。
我们使用事件来解耦,是为了考虑尽量避免RPC,简化系统依赖,减少外部服务不可用对系统模型带来的状态影响。所以领域事件强调的是高度自治,但是也需要斟酌,通过事件处理的情况必须是容许延时的,并且消息的接收方需要是一个幂等接收器(可以自幂等,或者对于重复消息的拒绝处理),因为消息是可能重复发送的。
上一篇: 由商品页的设计与实现说起
下一篇: 《高性能网站建设指南》读后总结