在微服务中使用领域事件
认识领域事件领域事件Domain Events是领域驱动设计Domain Driven DesignDDD中的一个概念用于捕获我们所建模的领域中所发生过的事情。领域事件本身也作为通用语言Ubiquitous Language的一部分成为包括领域专家在内的所有项目成员的交流用语。比如在用户注册过程中我们可能会说“当用户注册成功之后发送一封欢迎邮件给客户。”此时的“用户已经注册”便是一个领域事件。当然并不是所有发生过的事情都可以成为领域事件。一个领域事件必须对业务有价值有助于形成完整的业务闭环也即一个领域事件将导致进一步的业务操作。举个咖啡厅建模的例子当客户来到前台时将产生“客户已到达”的事件如果你关注的是客户接待比如需要为客户预留位置等那么此时的“客户已到达”便是一个典型的领域事件因为它将用于触发下一步——“预留位置”操作但是如果你建模的是咖啡结账系统那么此时的“客户已到达”便没有多大存在的必要——你不可能在用户到达时就立即向客户要钱对吧而”客户已下单“才是对结账系统有用的事件。在微服务Microservices架构实践中人们大量地借用了DDD中的概念和技术比如一个微服务应该对应DDD中的一个限界上下文Bounded Context在微服务设计中应该首先识别出DDD中的聚合根Aggregate Root还有在微服务之间集成时采用DDD中的防腐层Anti-Corruption Layer, ACL我们甚至可以说DDD和微服务有着天生的默契。更多有关DDD的内容请参考笔者的另一篇文章或参考《领域驱动设计》及《实现领域驱动设计》。在DDD中有一条原则一个业务用例对应一个事务一个事务对应一个聚合根也即在一次事务中只能对一个聚合根进行操作。但是在实际应用中我们经常发现一个用例需要修改多个聚合根的情况并且不同的聚合根还处于不同的限界上下文中。比如当你在电商网站上买了东西之后你的积分会相应增加。这里的购买行为可能被建模为一个订单Order对象而积分可以建模成账户Account对象的某个属性订单和账户均为聚合根并且分别属于订单系统和账户系统。显然我们需要在订单和积分之间维护数据一致性然而在同一个事务中同时更新两者又违背了DDD设计原则并且此时需要在两个不同的系统之间采用重量级的分布式事务Distributed Transactioin也叫XA事务或者全局事务。另外这种方式还在订单系统和账户系统之间产生了强耦合。通过引入领域事件我们可以很好地解决上述问题。总的来说领域事件给我们带来以下好处解耦微服务限界上下文帮助我们深入理解领域模型提供审计和报告的数据来源迈向事件溯源Event Sourcing和CQRS等还是以上面的电商网站为例当用户下单之后订单系统将发出一个“用户已下单”的领域事件并发布到消息系统中此时下单便完成了。账户系统订阅了消息系统中的“用户已下单”事件当事件到达时进行处理提取事件中的订单信息再调用自身的积分引擎也有可能是另一个微服务计算积分最后更新用户积分。可以看到此时的订单系统在发送了事件之后整个用例操作便结束了根本不用关心是谁收到了事件或者对事件做了什么处理。事件的消费方可以是账户系统也可以是任何一个对事件感兴趣的第三方比如物流系统。由此各个微服务之间的耦合关系便解开了。值得注意的一点是此时各个微服务之间不再是强一致性而是基于事件的最终一致性。事件风暴Event Storming事件风暴是一项团队活动旨在通过领域事件识别出聚合根进而划分微服务的限界上下文。在活动中团队先通过头脑风暴的形式罗列出领域中所有的领域事件整合之后形成最终的领域事件集合然后对于每一个事件标注出导致该事件的命令Command再然后为每个事件标注出命令发起方的角色命令可以是用户发起也可以是第三方系统调用或者是定时器触发等。最后对事件进行分类整理出聚合根以及限界上下文。事件风暴还有一个额外的好处是可以加深参与人员对领域的认识。需要注意的是在事件风暴活动中领域专家是必须在场的。更多有关事件风暴的内容请参考这里。创建领域事件领域事件应该回答“什么人什么时候做了什么事情”这样的问题在实际编码中可以考虑采用层超类型(Layer Supertype)来包含事件的某些共有属性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等。还是以上面的“注册后发送欢迎邮件”为例注册行为和发送邮件行为虽然通过领域事件集成但是他们依然发生在同一个线程中并且是同步的。另外需要注意的是在限界上下文之内使用领域事件时我们依然需要遵循“一个事务只更新一个聚合根”的原则违反之往往意味着我们对聚合根的拆分是错的。即便确实存在这样的情况也应该通过异步的方式此时需要引入消息中间件对不同的聚合根采用不同的事务此时可以考虑使用后台任务。除了用于微服务的内部领域事件更多的是被用于集成不同的微服务如上文中的“电商订单”例子。