李锋镝的博客

  • 首页
  • 时间轴
  • 说说
  • 每日心情
  • Now
  • 系列文章
  • 论坛
  • 左邻右舍
    • 左邻右舍
    • 博友圈
  • 留言
    • 留言
    • 走心评论
  • 关于
    • 关于我
    • 网站地图
    • 网站统计
    • 另一个网站
    • 我的导航站
    • 赞助
  • 🚇开往
Destiny
自是人生长恨水长东
  1. 首页
  2. 转载
  3. 技术
  4. 正文

在微服务中使用领域事件

2019年11月11日 约 4,577 字16 分钟 122 0 0
本文最后更新于 2019年11月11日,距今已 2494 天,其中的信息可能已经发生变化,请注意甄别。
稍微回想一下计算机硬件的工作原理我们便不难发现,整个计算机的工作过程其实就是一个对事件的处理过程。当你点击鼠标、敲击键盘或者插上U盘时,计算机便以中断的形式处理各种外部事件。在软件开发领域,事件驱动架构(Event Driven Architecture,EDA)早已被开发者用于各种实践,典型的应用场景比如浏览器对用户输入的处理、消息机制以及SOA。最近几年重新进入开发者视野的响应式编程(Reactive Programming)更是将事件作为该编程模型中的一等公民。可见,“事件”这个概念一直在计算机科学领域中扮演着重要的角色。 
在微服务中使用领域事件
认识领域事件 
领域事件(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等)。还是以上面的“注册后发送欢迎邮件”为例,注册行为和发送邮件行为虽然通过领域事件集成,但是他们依然发生在同一个线程中,并且是同步的。另外需要注意的是,在限界上下文之内使用领域事件时,我们依然需要遵循“一个事务只更新一个聚合根”的原则,违反之往往意味着我们对聚合根的拆分是错的。即便确实存在这样的情况,也应该通过异步的方式(此时需要引入消息中间件)对不同的聚合根采用不同的事务,此时可以考虑使用后台任务。
除了用于微服务的内部,领域事件更多的是被用于集成不同的微服务,如上文中的“电商订单”例子。 
在微服务中使用领域事件
通常,领域事件产生于领域对象中,或者更准确的说是产生于聚合根中。在具体编码实现时,有多种方式可用于发布领域事件。 
一种直接的方式是在聚合根中直接调用发布事件的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());
    }
}
这种方法虽然避免了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,何乐而不为? 
总结
领域事件主要用于解耦微服务,此时各个微服务之间将形成最终一致性。事件风暴活动有助于我们对微服务进行拆分,并且有助于我们深入了解某个领域。领域事件作为已经发生过的历史数据,在建模时应该将其创建为不可变的特殊值对象。存在多种方式用于发布领域事件,其中“在聚合中临时保存领域事件”的方式是值得推崇的。另外,我们需要考虑到聚合更新和事件发布之间的原子性,可以考虑使用XA事务或者采用单独的事件表。为了避免事件重复带来的问题,最好的方式是将事件的消费方创建为幂等的。
原文地址:https://www.cnblogs.com/davenkin/p/6612656.html

参考链接

26
  1. 1事件驱动架构en.wikipedia.org
  2. 2响应式编程en.wikipedia.org
  3. 3领域事件martinfowler.com
  4. 4领域驱动设计en.wikipedia.org
  5. 5微服务martinfowler.com
  6. 6另一篇www.infoq.com
  7. 7《领域驱动设计》book.douban.com
  8. 8《实现领域驱动设计》book.douban.com
  9. 9分布式事务en.wikipedia.org
  10. 10事件martinfowler.com
  11. 11CQRSmartinfowler.com
  12. 12这里techbeacon.com
  13. 13层超类型martinfowler.com
  14. 14不可变的en.wikipedia.org
  15. 15另一种方法udidahan.com
  16. 16Udi Dahanudidahan.com
  17. 17在聚合根中临时保存领域事件lostechies.com
  18. 18在聚合根方法中直接返回领域事件blog.jayway.com
  19. 19JTAen.wikipedia.org
  20. 20Atomikoswww.atomikos.com
  21. 21Bitronixgithub.com
  22. 22支持docs.spring.io
  23. 23幂等www.enterpriseintegrationpatterns.com
  24. 24版本9.4www.postgresql.org
  25. 25版本5.7dev.mysql.com
  26. 26https://www.cnblogs.com/davenkin/p/6612656.htmlwww.cnblogs.com
除非注明,否则均为李锋镝的博客原创文章,转载必须以链接形式标明本文链接

本文链接:https://www.lifengdi.com/transport/1501

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
分享到

在微服务中使用领域事件

也可使用浏览器菜单中的「分享」功能

微信扫一扫分享

标签: 微服务 领域模型
最后更新:2019年11月11日

岁月同一天 9 月 9 日

回望过去的今天,你在写什么

  • 5 年前 2021年9月9日
    百度的索引量数据现在有点儿太夸张了吧?

    咱的小破站怎么可能有这么多的索引量…… 是不是谁给咱洗数据了? :huaji3: 

相关文章
  • 一文详解微服务架构2019年10月9日
  • Java中PO、VO、BO、DTO、POJO、DAO释义2019年6月28日
  • 如何提升微服务的幸福感2020年5月22日
  • 微服务架构梳理总结2019年6月13日
  • 微服务架构梳理总结(二)2019年6月13日
blank

李锋镝

既然选择了远方,便只顾风雨兼程。

打赏 点赞
< 上一篇
下一篇 >
1234567891112131415161718192021222324252627282930313233343536373839404142434446474849505152535455575859606162636465666769727476777879808182858687909293949596979899
取消回复

文章评论

还没有评论,快来抢沙发吧~

横眉冷对千夫指,俯首甘为孺子牛。

听点儿音乐吧 朋友~
文章目录
最新 热点 随机
最新 热点 随机
推荐一个SVG 矢量小图标免费下载网站 关于使用AI的一些思考 book-to-skill:GitHub2.7万star开源项目,将书籍蒸馏为Agent可调用技能 最近厄尔尼诺现象越来越猛了 Kratos-plus v1.1.20版本更新说明 实用skills介绍之:Ponytail
给主题增加了Now、每日心情、年度回顾、岁月同一天、随机漫步等功能Kratos+ v1.1.16版本更新说明AI时代,个人技术博客的出路在哪里?增加了两套复古皮肤-牛皮纸、千禧网页写了一个订阅每日新闻的WP插件WordPress缓存插件WP Fastest Cache、WP Rocket 、FlyingPress对比
Dylan Custom Plugin 1.0.2版本更新说明 成语接龙 开发者必懂的 AI 向量入门:从数学基础到实战应用 8条经验轻松上手IDEA插件开发 如何通过命令查看Java应用内存中对象数量 规范驱动AI编程:用OpenSpec实现100%可控开发,从需求到代码的全流程闭环
最近评论
blank
李锋镝 发布于 3 小时前(09月09日) 是的,AI的执行力实在是太强了~ :79:
blank
obaby 发布于 5 小时前(09月09日) ai为了提高评分,会入侵服务器。哈哈哈 其实也正常。 目标导向就是如此。
blank
李锋镝 发布于 23 小时前(09月08日) 主要是看见这个配色想要,看见那个配色想要,干脆就都加里面了
blank
威言威语 发布于 24 小时前(09月08日) 配色是挺丰富~
blank
李锋镝 发布于 1 天前(09月08日) 让AI根据代码提交记录组织一下,给AI定一个规则,然后就轻松了
标签聚合
K8s 多线程 SQL 分布式 AI ElasticSearch Spring MySQL 架构 MQ 数据库 AI编程 JVM 日常 Claude SpringBoot JAVA Redis WordPress IDEA
友情链接
  • 韩情脉脉
  • 搬砖日记
  • 志文工作室
  • Mr.Sun的博客
  • Serendipity
  • 九仞之行
  • 老张博客
  • 若梦博客
  • 林羽凡
  • sssr7844的博客
  • 彬红茶日记
  • Honesty
  • 懋和道人
  • 韩小韩博客
  • 皮皮社
  • 蜗牛工作室
  • 瓦匠个人小站
  • 临窗旋墨
  • 知向前端
  • 哥斯拉

COPYRIGHT © 2026 lifengdi.com. ALL RIGHTS RESERVED.

正在博友圈履约中

Domain age badge for lifengdi.com

Theme Kratos-plus By Dylan Li

津ICP备2024022503号-3

京公网安备11011502039375号