后端开发实践系列——事件驱动架构(EDA)编码实践

本文是后端开发实践系列的一部分,探讨了如何落地事件驱动架构(EDA)。作者分析了EDA未普及的原因,并提供了领域事件的建模方法,包括创建、发布和消费领域事件。文章通过一个基于RabbitMQ的微服务示例,阐述了事件驱动架构的两种风格,并给出了具体的配置和系统演示。最后,展示了如何在实际项目中采用RabbitMQ实现事件驱动,包括消息流向和异常处理策略。
摘要由CSDN通过智能技术生成

在本系列的前两篇文章中,笔者分别讲到了后端项目的代码模板DDD编码实践,在本文中,我将继续以编码实践的方式分享如何落地事件驱动架构。

单纯地讲事件驱动架构(Event Driven Architecture, EDA),那是几十年前就出现了的话题;单纯地讲领域事件,那也是这些年被大量提及并讨论得快熟透了的软件用语。然而,就笔者的观察看,事件驱动架构远没有想象中那样普遍地被开发团队所接受。即便搞微服务的人都知道除了同步的HTTP还有异步的消息机制,即便搞DDD的人都知道领域事件是其中的一等公民,事件驱动架构所带来的优点并没有相应地转化为软件从业者的青睐。

我尝试着去思考其中的原因,总结出了两点:第一是事件驱动可能是客观世界的运作方式,但不是人的自然思考问题的方式;第二是事件驱动架构在给软件带来好处的同时,又会增加额外的复杂性,比如调试的困难性,又比如并不直观的最终一致性。

当然,事实上有不少软件项目都使用了消息队列,但是这里需要明确的是,对消息队列的使用并不意味着你的项目就一定是事件驱动架构,很多项目只是由于技术方面的驱动,小范围地采用了某些消息队列(比如RabbitMQ和Kafka等)的产品而已。偌大一个系统,如果你的消息队列只是用作邮件发送的通知,那么这样系统自然谈不上采用了事件驱动架构。

放到当下,微服务兴起,DDD重现,在采用事件驱动架构时, 我们需要考虑业务的建模、领域事件的设计、DDD的约束、限界上下文的边界以及更多技术方面的因素,这一个系统工程应该如何从头到尾的落地,是需要经过思考和推敲的。还是那句话,有讲究的编程并不是一件易事。

诚然,用好事件驱动架构存在实践上的难处,然而它的优点也委实诱人,本文希望形成一定的“条理”和“套路”,让事件驱动架构能够更简单的落地。

本文主要分为两大部分,第一部分独立于具体的消息队列实现来讲解通用的对领域事件的建模,第二部分以一个真实的微服务系统为例,采用RabbitMQ作为消息队列,并以此分享完整的事件驱动架构落地实践。

本文以DDD为基础进行编码,其中会涉及到DDD中的不少概念,比如聚合根、资源库和应用服务等,对DDD不熟悉的读者可以参考笔者的DDD编码实践文章

本文的示例代码请参考github上的e-commerce-sample项目。


第一部分:领域事件的建模

领域事件是DDD中的一个概念,表示的是在一个领域中所发生的一次对业务有价值的事情,落到技术层面就是在一个业务实体对象(通常来说是聚合根)的状态发生了变化之后需要发出一个领域事件。虽然事件驱动架构中的“事件”不一定指“领域事件”,但本文由于密切结合DDD,因此当提到事件时,我们特指“领域事件”。


创建领域事件

关于领域事件的基础知识,请参考笔者的在微服务中使用领域事件文章,本文直接进入编码实践环节。

在建模领域事件时,首先需要记录事件的一些通用信息,比如唯一标识ID和创建时间等,为此创建事件基类DomainEvent

public abstract class DomainEvent {
    private final String _id;
    private final DomainEventType _type;
    private final Instant _createdAt;
}

在DDD场景下,领域事件一般随着聚合根状态的更新而产生,另外,在事件的消费方,有时我们希望监听发生在某个聚合根下的所有事件,为此笔者建议为每一个聚合根对象创建相应的事件基类,其中包含聚合根的ID,比如对于订单(Order)类,创建OrderEvent

public abstract class OrderEvent extends DomainEvent {
    private final String orderId;
}

然后对于实际的Order事件,统一继承自OrderEvent,比如对于创建订单的OrderCreatedEvent事件:

public class OrderCreatedEvent extends OrderEvent {
    private final BigDecimal price;
    private final Address address;
    private final List<OrderItem> items;
    private final Instant createdAt;
}

领域事件的继承链如下:

在创建领域事件时,需要注意2点:

  • 领域事件本身应该是不变的(Immutable);
  • 领域事件应该携带与事件发生时相关的上下文数据信息,但是并不是整个聚合根的状态数据,例如,在创建订单时可以携带订单的基本信息,而对于产品(Product)名称更新的ProductNameUpdatedEvent事件,则应该同时包含更新前后的产品名称:
public class ProductNameUpdatedEvent extends ProductEvent {
    private String oldName; //更新前的名称
    private String newName; // 更新后的名称
}

发布领域事件

发布领域事件有多种方式,比如可以在应用服务(ApplicationService)中发布,也可以在资源库(Repository)中发布,还可以引入事件表的方式,这3种发布方式的详细比较可以参考笔者的在微服务中使用领域事件文章。笔者建议采用事件表方式,这里展开讨论一下。

通常的业务处理过程都会更新数据库然后发布领域事件,这里一个比较重要的点是:我们需要保证数据库更新和事件发布之间的原子性,也即要么二者都成功,要么都失败。在传统的实践方式中,全局事务(Global Transaction/XA Transaction)通常用于解决此类问题。然而,全局事务本身的效率是很低的,另外,一些技术框架并不提供对全局事务的支持。当前,一种比较受推崇的方式是引入事件表,其流程大致如下:

  1. 在更新业务表的同时,将领域事件一并保存到数据库的事件表中,此时业务表和事件表在同一个本地事务中,即保证了原子性,又保证了效率。

  2. 在后台开启一个任务,将事件表中的事件发布到消息队列中,发送成功之后删除掉事件。

但是,这里又有一个问题:在第2步中,我们如何保证发布事件和删除事件之间的原子性呢?答案是:我们不用保证它们的原子性,我们需要保证的是“至少一次投递”,并且保证消费方幂等。此时的大致场景如下:

  • 代码中先发布事件,成功后再从事件表中删除事件;
  • 发布消息成功,事件删除也成功,皆大欢喜;
  • 如果消息发布不成功,那么代码中不会执行事件删除逻辑,就像事情没有发生一样,一致性得到保证;
  • 如果消息发布成功,但是事件删除失败,那么在第二次任务执行时,会重新发布消息,导致消息的重复发送。然而,由于我们要求了消费方的幂等性,也即消费方多次消费同一条消息是ok的,整个过程的一致性也得到了保证。

发布领域事件的整个流程如下:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值