回顾计算机硬件的工作原理,不难发现计算机的整个工作过程其实就是一个事件处理的过程。当你点击鼠标、敲击键盘或插入U盘时,计算机会以中断的形式处理各种外部事件。在软件开发领域,事件驱动架构(EDA)早已被开发人员用于各种实践。典型的应用场景包括浏览器处理用户输入、消息机制、SOA。近年来重新进入开发者视野的ReactiveProgramming将事件视为编程模型中的一等公民。可见,“事件”的概念在计算机科学领域一直扮演着重要的角色。理解领域事件领域事件(DomainEvents)是领域驱动设计(DDD)中的一个概念,它用来捕捉我们正在建模的领域中发生了什么。领域事件本身作为通用语言的一部分,成为所有项目成员(包括领域专家)的交流语言。比如在用户注册的过程中,我们可能会说“当用户注册成功后,给客户发送一封欢迎邮件”。此时,“用户已注册”是一个领域事件。当然,并非所有发生的事情都可以是领域事件。一个领域事件必须对业务有价值,并有助于形成一个完整的业务闭环,即一个领域事件将导致进一步的业务运作。举个咖啡店建模的例子,当有顾客来到前台时,会产生“顾客已到”的事件。如果你关注客户接待,比如需要为客户预留位置,那么此时“客户已到”是一个典型的领域事件,因为它会被用来触发下一步——“预留位置”一个地方”动作;但是如果你正在为一个咖啡结账系统建模,“顾客已经到了”在这一点上并没有太多必要的——你不能在用户一到就向顾客要钱,是的,“客户已下订单”对于结账系统来说是一个有用的事件。在微服务(Microservices)架构的实践中,人们借鉴了DDD中的很多概念和技术。比如一个微服务应该对应DDD中的一个限界上下文(BoundedContext);聚合根(AggregateRoot);以及微服务之间集成时DDD中的反腐层(Anti-CorruptionLayer,ACL);甚至可以说DDD和微服务有着天然的默契。关于DDD的更多信息,请参考作者的另一篇文章或参考《领域驱动设计》和《实现领域驱动设计》。DDD中有一个原则:一个业务用例对应一个事务,一个事务对应一个聚合根,即在一个事务中,只能操作一个聚合根。但是在实际应用中,我们经常会发现一个用例需要修改多个聚合根,而不同的聚合根仍然处于不同的限界上下文中。比如你在电商网站上买东西,你的积分就会相应增加。这里的购买行为可以建模为Order对象,积分可以建模为Account对象的一个??属性。订单和账户都是聚合根,分别属于订单系统和账户系统。显然,我们需要保持订单和积分之间的数据一致性。但是在同一个事务中同时更新两者违反了DDD的设计原则,这时候就需要在两个不同的系统之间采用一个重量级的分布式系统。事务(DistributedTransaction,又称XA事务或全局事务)。另外,这种方式还造成了订单系统和账户系统之间的强耦合。通过引入领域事件,我们可以很好地解决上述问题。总的来说,领域事件给我们带来以下好处:解耦微服务(限界上下文)帮助我们深入理解领域模型提供审计和报表数据源走向事件溯源(EventSourcing)和CQRS等或以上走电子商务网站为例。用户下单后,订单系统会发送“用户已下单”的字段事件发布到消息系统。这个时候,订单就完成了。账户系统在消息系统中订阅“用户已下单”事件,当事件到达时进行处理,提取事件中的订单信息,然后调用自己的积分引擎(或其他微服务)计算积分,***更新用户积分。可以看到,此时订单系统发送完事件后,整个用例操作就结束了,不需要关心谁收到了事件,也不需要关心事件做了什么处理。事件的消费者可以是账户系统,也可以是任何对事件感兴趣的第三方,比如物流系统。从而解开了各个微服务之间的耦合关系。值得注意的是,此时微服务之间并没有强一致性,而是基于事件的最终一致性。事件风暴(EventStorming)事件风暴是一种团队活动,旨在通过领域事件识别聚合根,进而划分微服务的有界上下文。活动中,团队首先通过头脑风暴列出领域内的所有领域事件,然后将它们进行整合,形成最终的领域事件集合。然后,对于每一个事件,它都标记了引起该事件的命令(Command),然后对于每一个事件标记了命令发起者的角色。该命令可以由用户发起,也可以由第三方系统调用或由定时器触发。***对事件进行分类以整理聚合根和有界上下文。事件风暴的另一个好处是可以加深相关人员的领域知识。应该注意的是,在事件风暴活动期间,领域专家必须在场。创建领域事件领域事件应该回答诸如“谁在什么时候做了什么”这样的问题。在实际编码中,可以认为层超类型(LayerSupertype)包含一些事件的公共属性:publicabstractclassEvent{privatefinalUUIDid;私人最终日期时间创建时间;publicEvent(){this.id=UUID.randomUUID();this.createdTime=newDateTime();}}可以看到,领域事件也包含了ID,但是ID并不是实体(Entity)级别的ID概念,主要用于事件追踪和日志记录。另外,由于领域事件描述的是过去发生的事情,我们应该将领域事件建模为不可变的(Immutable)。从DDD的概念来看,领域事件更像是一个特殊的值对象(ValueObject)。对于上面提到的咖啡馆示例,创建“顾客已到达”事件如下:publicfinalclassCustomerArrivedEventextendsEvent{publicCustomerArrivedEvent(intcustomerNumber){super();this.customerNumber=customerNumber;}}在这个CustomerArrivedEvent事件中,除了继承自Event的属性外,还自定义了一个与事件密切相关的业务属性——客户数(customerNumber)——以便后续操作可以预留相应的座位数。另外,我们将所有的属性和CustomerArrivedEvent本身声明为final,不暴露任何可能修改这些属性的方法,从而保证了事件的不变性。发布领域事件在使用领域事件时,我们通常使用“发布-订阅”的方式来集成不同的模块或系统。在单个微服务中,我们可以使用领域事件来集成不同的功能组件。例如上面提到的“用户注册后向用户发送欢迎邮件”的例子,注册组件发出一个事件,邮件发送组件收到这个事件后向用户发送一封邮件。在微服务内部使用领域事件时,我们不一定要引入消息中间件(如ActiveMQ等)。仍以上述“注册后发送欢迎邮件”为例。虽然注册行为和发送邮件行为通过领域事件整合在一起,但它们仍然发生在同一个线程中,并且是同步的。另外需要注意的是,在限界上下文中使用领域事件时,我们仍然需要遵循“一个事务只更新一个聚合根”的原则。违反这一点通常意味着我们对聚合根的拆分是错误的。即使确实存在这样的情况,也应该以异步的方式针对不同的聚合根使用不同的事务(此时需要引入消息中间件),此时可以考虑后台任务。领域事件除了用在微服务内部,更多时候是用来整合不同的微服务,比如上面的“电商订单”例子。通常,领域事件源自领域对象,或者更准确地说,源自聚合根。在具体的编码实现中发布领域事件的方式有很多种。一种直接的方式是直接调用聚合根中发布事件的Service对象。以上面的“电商订单”为例,在创建订单时,会发布领域事件“ordercreated”。此时,可以考虑在订单对象的构造函数中发布事件:}}注意:为了专注于事件发布,我们简化了Order对象,实际编码中并没有引用Order对象本身。可以看出,为了发布OrderPlacedEvent事件,我们需要传入Service对象EventPublisher,这显然是一种API污染,即Order作为领域对象只需要关注与业务相关的数据,而不是EventPublisher等基础设施对象。NServiceBus的创始人UdiDahan提出了另一种方法,就是通过调用EventPublisher上的静态方法在领域对象中发布领域事件:publicclassOrder{publicOrder(){//createorder//...EventPublisher.publish(新的OrderPlacedEvent());}}这种方法虽然避免了API污染,但是这里的publish()静态方法会产生副作用,给Order对象的测试带来困难。此时,我们可以通过“在聚合根中临时保存领域事件”来改进:publicOrder(){//创建订单//...events.add(newOrderPlacedEvent());}publicList
