在微服务中使用领域事件

稍微回想一下计算机硬件的工做原理咱们便不难发现,整个计算机的工做过程其实就是一个对事件的处理过程。当你点击鼠标、敲击键盘或者插上U盘时,计算机便以中断的形式处理各类外部事件。在软件开发领域,事件驱动架构(Event Driven Architecture,EDA)早已被开发者用于各类实践,典型的应用场景好比浏览器对用户输入的处理、消息机制以及SOA。最近几年从新进入开发者视野的响应式编程(Reactive Programming)更是将事件做为该编程模型中的一等公民。可见,“事件”这个概念一直在计算机科学领域中扮演着重要的角色。数据库

 

 

 

若是想学习Java工程化、高性能及分布式、深刻浅出。微服务、Spring,MyBatis,Netty源码分析的朋友能够加个人Java高级交流:854630135,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给你们。编程

认识领域事件浏览器

领域事件(Domain Events)是领域驱动设计(Domain Driven Design,DDD)中的一个概念,用于捕获咱们所建模的领域中所发生过的事情。领域事件自己也做为通用语言(Ubiquitous Language)的一部分红为包括领域专家在内的全部项目成员的交流用语。好比,在用户注册过程当中,咱们可能会说“当用户注册成功以后,发送一封欢迎邮件给客户。”,此时的“用户已经注册”即是一个领域事件。服务器

 

固然,并非全部发生过的事情均可以成为领域事件。一个领域事件必须对业务有价值,有助于造成完整的业务闭环,也即一个领域事件将致使进一步的业务操做。举个咖啡厅建模的例子,当客户来到前台时将产生“客户已到达”的事件,若是你关注的是客户接待,好比须要为客户预留位置等,那么此时的“客户已到达”即是一个典型的领域事件,由于它将用于触发下一步——“预留位置”操做;可是若是你建模的是咖啡结帐系统,那么此时的“客户已到达”便没有多大存在的必要——你不可能在用户到达时就当即向客户要钱对吧,而”客户已下单“才是对结帐系统有用的事件。架构

 

在微服务(Microservices)架构实践中,人们大量地借用了DDD中的概念和技术,好比一个微服务应该对应DDD中的一个限界上下文(Bounded Context);在微服务设计中应该首先识别出DDD中的聚合根(Aggregate Root);还有在微服务之间集成时采用DDD中的防腐层(Anti-Corruption Layer, ACL);咱们甚至能够说DDD和微服务有着天生的默契。更多有关DDD的内容,请参考笔者的另外一篇文章或参考《领域驱动设计》及《实现领域驱动设计》。并发

 

在DDD中有一条原则:一个业务用例对应一个事务,一个事务对应一个聚合根,也即在一次事务中,只能对一个聚合根进行操做。可是在实际应用中,咱们常常发现一个用例须要修改多个聚合根的状况,而且不一样的聚合根还处于不一样的限界上下文中。好比,当你在电商网站上买了东西以后,你的积分会相应增长。这里的购买行为可能被建模为一个订单(Order)对象,而积分能够建模成帐户(Account)对象的某个属性,订单和帐户均为聚合根,而且分别属于订单系统和帐户系统。显然,咱们须要在订单和积分之间维护数据一致性,然而在同一个事务中同时更新二者又违背了DDD设计原则,而且此时须要在两个不一样的系统之间采用重量级的分布式事务(Distributed Transactioin,也叫XA事务或者全局事务)。另外,这种方式还在订单系统和帐户系统之间产生了强耦合。经过引入领域事件,咱们能够很好地解决上述问题。框架

 

总的来讲,领域事件给咱们带来如下好处:dom

  1. 解耦微服务(限界上下文)

 

  1. 帮助咱们深刻理解领域模型

 

  1. 提供审计和报告的数据来源

 

  1. 迈向事件溯源(Event Sourcing)和CQRS等

 

 

仍是以上面的电商网站为例,当用户下单以后,订单系统将发出一个“用户已下单”的领域事件,并发布到消息系统中,此时下单便完成了。帐户系统订阅了消息系统中的“用户已下单”事件,当事件到达时进行处理,提取事件中的订单信息,再调用自身的积分引擎(也有多是另外一个微服务)计算积分,最后更新用户积分。能够看到,此时的订单系统在发送了事件以后,整个用例操做便结束了,根本不用关心是谁收到了事件或者对事件作了什么处理。事件的消费方能够是帐户系统,也能够是任何一个对事件感兴趣的第三方,好比物流系统。由此,各个微服务之间的耦合关系便解开了。值得注意的一点是,此时各个微服务之间再也不是强一致性,而是基于事件的最终一致性。异步

 

 

 

 

 

事件风暴(Event Storming)分布式

事件风暴是一项团队活动,旨在经过领域事件识别出聚合根,进而划分微服务的限界上下文。在活动中,团队先经过头脑风暴的形式罗列出领域中全部的领域事件,整合以后造成最终的领域事件集合,而后对于每个事件,标注出致使该事件的命令(Command),再而后为每一个事件标注出命令发起方的角色,命令能够是用户发起,也能够是第三方系统调用或者是定时器触发等。最后对事件进行分类整理出聚合根以及限界上下文。事件风暴还有一个额外的好处是能够加深参与人员对领域的认识。须要注意的是,在事件风暴活动中,领域专家是必须在场的。更多有关事件风暴的内容,请参考这里。

 

 

 

 

 

 

 

建立领域事件

领域事件应该回答“什么人何时作了什么事情”这样的问题,在实际编码中,能够考虑采用层超类型(Layer Supertype)来包含事件的某些共有属性:

 

若是想学习Java工程化、高性能及分布式、深刻浅出。微服务、Spring,MyBatis,Netty源码分析的朋友能够加个人Java高级交流:854630135,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给你们。

 
public abstract class Event { private final UUID id; private final DateTime createdTime; public Event() { this.id = UUID.randomUUID(); this.createdTime = new DateTime(); } }

 

 

 

能够看到,领域事件还包含了ID,可是该ID并非实体(Entity)层面的ID概念,而是主要用于事件追溯和日志。另外,因为领域事件描述的是过去发生的事情,咱们应该将领域事件建模成不可变的(Immutable)。从DDD概念上讲,领域事件更像一种特殊的值对象(Value Object)。对于上文中提到的咖啡厅例子,建立“客户已到达”事件以下:

 

 

 
public final class CustomerArrivedEvent extends Event { private final int customerNumber; public CustomerArrivedEvent(int customerNumber) { super(); this.customerNumber = customerNumber; } }

 

 

 

在这个CustomerArrivedEvent事件中,除了继承自Event的属性外,还自定义了一个与该事件密切关联的业务属性——客户人数(customerNumber)——这样后续操做即可预留相应数目的座位了。另外,咱们将全部属性以及CustomerArrivedEvent自己都声明成了final,而且不向外暴露任何可能修改这些属性的方法,这样便保证了事件的不变性。

 

 

发布领域事件

在使用领域事件时,咱们一般采用“发布-订阅”的方式来集成不一样的模块或系统。在单个微服务内部,咱们可使用领域事件来集成不一样的功能组件,好比在上文中提到的“用户注册以后向用户发送欢迎邮件”的例子中,注册组件发出一个事件,邮件发送组件接收到该事件后向用户发送邮件。

 

 

 

 

 

在微服务内部使用领域事件时,咱们不必定非得引入消息中间件(好比ActiveMQ等)。仍是以上面的“注册后发送欢迎邮件”为例,注册行为和发送邮件行为虽然经过领域事件集成,可是他们依然发生在同一个线程中,而且是同步的。另外须要注意的是,在限界上下文以内使用领域事件时,咱们依然须要遵循“一个事务只更新一个聚合根”的原则,违反之每每意味着咱们对聚合根的拆分是错的。即使确实存在这样的状况,也应该经过异步的方式(此时须要引入消息中间件)对不一样的聚合根采用不一样的事务,此时能够考虑使用后台任务。

 

除了用于微服务的内部,领域事件更多的是被用于集成不一样的微服务,如上文中的“电商订单”例子。

 

 

 

 

若是想学习Java工程化、高性能及分布式、深刻浅出。微服务、Spring,MyBatis,Netty源码分析的朋友能够加个人Java高级交流:854630135,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给你们。

一般,领域事件产生于领域对象中,或者更准确的说是产生于聚合根中。在具体编码实现时,有多种方式可用于发布领域事件。

 

一种直接的方式是在聚合根中直接调用发布事件的Service对象。以上文中的“电商订单”为例,当建立订单时,发布“订单已建立”领域事件。此时能够考虑在订单对象的构造函数中发布事件:

 

 

 
public class Order { public Order(EventPublisher eventPublisher) { //create order  //…  eventPublisher.publish(new OrderPlacedEvent()); } }

 

 

注:为了把焦点集中在事件发布上,咱们对Order对象作了简化,Order对象自己在实际编码中不具有参考性。

 

能够看到,为了发布OrderPlacedEvent事件,咱们须要将Service对象EventPublisher传入,这显然是一种API污染,即Order做为一个领域对象只须要关注和业务相关的数据,而不是诸如EventPublisher这样的基础设施对象。 另外一种方法是由NServiceBus的创始人Udi Dahan提出来的,即在领域对象中经过调用EventPublisher上的静态方法发布领域事件:

 

 
public class Order { public Order() { //create order //... EventPublisher.publish(new OrderPlacedEvent()); } }

 

若是想学习Java工程化、高性能及分布式、深刻浅出。微服务、Spring,MyBatis,Netty源码分析的朋友能够加个人Java高级交流:854630135,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给你们。

 

这种方法虽然避免了API污染,可是这里的publish()静态方法将产生反作用,对Order对象的测试带来了难处。此时,咱们能够采用“在聚合根中临时保存领域事件”的方式予以改进:

 

 

 
public class Order { private List<Event> events; public Order() { //create order //... events.add(new OrderPlacedEvent()); } public List<Event> getEvents() { return events; } public void clearEvents() { events.clear(); } }

 

 

 

在测试Order对象时,咱们便你能够经过验证events集合保证Order对象在建立时的确发布了OrderPlacedEvent事件:

 

 

 
@Test public void shouldPublishEventWhenCreateOrder() { Order order = new Order();  List<Event> events = order.getEvents();  assertEquals(1, events.size()); Event event = events.get(0); assertTrue(event instanceof OrderPlacedEvent); }

 

 

 

在这种方式中,聚合根对领域事件的保存只能是临时的,在对该聚合根操做完成以后,咱们应该将领域事件发布出去并及时清空events集合。能够考虑在持久化聚合根时进行这样的操做,在DDD中即为资源库(Repository):

 

 

 
public class OrderRepository { private EventPublisher eventPublisher; public void save(Order order) { //save the order //... List<Event> events = order.getEvents(); events.forEach(event -> eventPublisher.publish(event)); order.clearEvents(); } }

 

 

 

除此以外,还有一种与“临时保存领域事件”类似的作法是“在聚合根方法中直接返回领域事件”,而后在Repository中进行发布。这种方式依然有很好的可测性,而且开发人员不用手动清空先前的事件集合,不过仍是得记住在Repository中将事件发布出去。另外,这种方式不适合建立聚合根的场景,由于此时的建立过程既要返回聚合根自己,又要返回领域事件。

 

这种方式也有很差的地方,好比它要求开发人员在每次更新聚合根时都必须记得清空events集合,忘记这么作将为程序带来严重的bug。不过虽然如此,这依然是笔者比较推荐的方式。

 

业务操做和事件发布的原子性

虽然在不一样聚合根之间咱们采用了基于领域事件的最终一致性,可是在业务操做和事件发布之间咱们依然须要采用强一致性,也即这二者的发生应该是原子的,要么所有成功,要么所有失败,不然最终一致性根本无从谈起。以上文中“订单积分”为例,若是客户下单成功,可是事件发送失败,下游的帐户系统便拿不到事件,致使最终客户的积分并不增长。

 

要保证业务操做和事件发布之间的原子性,最直接的方法即是采用XA事务,好比Java中的JTA,这种方式因为其重量级并不被人们所看好。可是,对于一些对性能要求不那么高的系统,这种方式何尝不是一个选择。一些开发框架已经可以支持独立于应用服务器的XA事务管理器(如Atomikos 和Bitronix),好比Spring Boot做为一个微服务框架便提供了对Atomikos和Bitronix的支持。

 

若是JTA不是你的选项,那么能够考虑采用事件表的方式。这种方式首先将事件保存到聚合根所在的数据库中,因为事件表和聚合根表同属一个数据库,整个过程只须要一个本地事务就能完成。而后,在一个单独的后台任务中读取事件表中未发布的事件,再将事件发布到消息中间件中。

 

 

 

 

 

这种方式须要注意两个问题,第一个是因为发布了事件以后须要将表中的事件标记成“已发布”状态,即依然涉及到对数据库的操做,所以发布事件和标记“已发布”之间须要原子性。固然,此时依旧能够采用XA事务,可是这违背了采用事件表的初衷。一种解决方法是将事件的消费方建立成幂等的,即消费方能够屡次消费同一个事件。这个过程大体为:整个过程当中事件发送和数据库更新采用各自的事务管理,此时有可能发生的状况是事件发送成功而数据库更新失败,这样在下一次事件发布操做中,因为先前发布过的事件在数据库中依然是“未发布”状态,该事件将被从新发布到消息系统中,致使事件重复,但因为事件的消费方是幂等的,所以事件重复不会存在问题。

 

另一个须要注意的问题是持久化机制的选择。其实对于DDD中的聚合根来讲,NoSQL是相比于关系型数据库更合适的选择,好比用MongoDB的Document保存聚合根即是种很天然的方式。可是多数NoSQL是不支持ACID的,也就是说不能保证聚合更新和事件发布之间的原子性。还好,关系型数据库也在向NoSQL方向发展,好比新版本的PostgreSQL(版本9.4)和MySQL(版本5.7)已经可以提供具有NoSQL特征的JSON存储和基于JSON的查询。此时,咱们能够考虑将聚合根序列化成JSON格式的数据进行保存,从而避免了使用重量级的ORM工具,又能够在多个数据之间保证ACID,何乐而不为?

若是想学习Java工程化、高性能及分布式、深刻浅出。微服务、Spring,MyBatis,Netty源码分析的朋友能够加个人Java高级交流:854630135,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给你们。

总结

领域事件主要用于解耦微服务,此时各个微服务之间将造成最终一致性。事件风暴活动有助于咱们对微服务进行拆分,而且有助于咱们深刻了解某个领域。领域事件做为已经发生过的历史数据,在建模时应该将其建立为不可变的特殊值对象。存在多种方式用于发布领域事件,其中“在聚合中临时保存领域事件”的方式是值得推崇的。另外,咱们须要考虑到聚合更新和事件发布之间的原子性,能够考虑使用XA事务或者采用单独的事件表。为了不事件重复带来的问题,最好的方式是将事件的消费方建立为幂等的。

相关文章
相关标签/搜索