当前位置: 首页 > news >正文

在微服务中使用领域事件

认识领域事件

领域事件(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事务或者全局事务)。另外,这种方式还在订单系统和账户系统之间产生了强耦合。通过引入领域事件,我们可以很好地解决上述问题。

总的来说,领域事件给我们带来以下好处:

  1. 解耦微服务(限界上下文)
  1. 帮助我们深入理解领域模型
  1. 提供审计和报告的数据来源
  1. 迈向事件溯源(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等)。还是以上面的“注册后发送欢迎邮件”为例,注册行为和发送邮件行为虽然通过领域事件集成,但是他们依然发生在同一个线程中,并且是同步的。另外需要注意的是,在限界上下文之内使用领域事件时,我们依然需要遵循“一个事务只更新一个聚合根”的原则,违反之往往意味着我们对聚合根的拆分是错的。即便确实存在这样的情况,也应该通过异步的方式(此时需要引入消息中间件)对不同的聚合根采用不同的事务,此时可以考虑使用后台任务。

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

http://www.jsqmd.com/news/1106301/

相关文章:

  • 前面定义的一些类也要改一下。
  • NFD云解析插件扩展架构深度解析:从接口设计到实战实现
  • hybrik实时性测试
  • 数智中台全链路交付,赋能连锁零售规模化增长
  • Linux课后练习——管理“学习笔记”项目操作过程
  • 2026年真正免费的论文查重网站有哪些?7个平台实测+防骗指南
  • 如何快速修复ClusterGVis中箱线图与折线图显示冲突问题
  • TypeScript泛型
  • 【MO三维路径规划】麝牛算法MO多无人机协同集群避障路径规划(目标函数:最低成本:路径、高度、威胁、转角)【含Matlab源码 15684期】
  • langchain4j 学习系列(10)-Skill使用示例
  • LinuxShell编程基础学习笔记
  • 2026年无线物联网融合网络设备十大品牌排行榜
  • 量子优化算法FPC-QAOA:突破参数爆炸难题
  • 35岁转行AI大模型:挑战、机遇与实战路径
  • 服务端开发爱好者
  • 心情值游戏系统实现
  • [特殊字符] 搬砖的秘密:为什么一次搬 64 块砖最快?
  • 车间地坪养护秘籍
  • Rust项目开发完整教程
  • 从WAIC看AI办公新趋势:会议助手正在从“记录工具”变成“组织智能体”
  • Rust语言基础开发教程
  • 一个老股民的十年自白十年炒股没亏,但我劝你别学我
  • 本地化AI漫剧制作:Qwen与ComfyUI实战指南
  • 从 VMware 迁移到 Proxmox VE 的完整方案
  • MAX9744与PIC18LF45K50的音频功率放大系统设计
  • Vue组件开发技巧
  • 单系统登录机制
  • vim源码编译安装 _
  • 贵阳婚纱照拍的最好的是哪一家?
  • view_source