Spring Modulith 简化事件外发

工程 | Oliver Drotbohm | 2023 年 9 月 22 日 | ...

事务性服务方法在 Spring 应用中是一种常见的模式。这些方法会触发对业务至关重要的状态转换。这通常涉及核心领域抽象,例如聚合及其对应的存储库。这种安排的一个典型示例可能如下所示:

@Service
@RequiredArgsConstructor
class OrderManagement {

  private final OrderRepository orders;

  @Transactional
  Order complete(Order order) {
     return orders.save(order.complete());
  }
}

由于像这样的状态转换可能对第三方感兴趣,我们可能会希望引入消息代理,以发布消息供其他系统广泛分发。实现这一点的朴素方法是将这种交互隐藏在另一个 Spring 服务中,将其注入到我们的主要 Bean 中,并调用一个最终会与代理交互的方法。

@Service
@RequiredArgsConstructor
class OrderManagement {

  private final OrderRepository orders;
  private final MessageSender sender;

  @Transactional
  Order complete(Order order) {

     var result = orders.save(order.complete());

     sender.publishMessage(…);

     return result;
  }
}

问题

不幸的是,这种方法存在各种问题。

  1. 由于方法在事务中运行,它已经获取了一个数据库连接。与其他基础设施的交互成本很高,因此很可能显著延长事务的长度,阻止连接被提前返回,这可能导致连接池饱和,从而导致性能下降。
  2. 虽然我们优雅地将与代理的交互封装在一个看起来不错的外观后面,但我们的 completeOrder(…) 方法现在更容易受到更多基础设施问题的影响。无法访问代理会导致事务回滚,并阻止订单完成。由于下游基础设施问题,我们的系统可能在技术上可用,但完全无法执行任何有用的操作。
  3. 最后,在消息发布成功但数据库事务最终回滚的情况下,我们创建了一个一致性问题。

解决这些问题的一个常见模式是从服务中发布应用程序事件,乍一看,这与我们之前概述的并没有太大区别。

@Service
@RequiredArgsConstructor
class OrderManagement {

  private final OrderRepository orders;
  private final ApplicationEventPublisher events; 

  @Transactional
  Order complete(Order order) {

     var result = orders.save(order.complete());

     events.publishEvent(
         new OrderCompleted(result.getId(), result.getCustomerId()));

     return result;
  }

  record OrderCompleted(OrderId orderId, CustomerId customerId) {}
}

主要区别在于,首先,发布的事件是一个在 JVM 内部 传递的简单对象。与代理的实际交互随后将在 @Async @TransactionalEventListener 中实现。默认情况下,此类侦听器将在原始业务事务提交后被调用,这解决了问题 3。将侦听器标记为 @Async 会导致事件处理在单独的线程上执行,从而解决了问题 1。

Spring Modulith 事件外发

侦听器的实现是一项相当平凡的任务:我们必须选择一个特定于代理的客户端(Spring Kafka、Spring AMQP、JMS 等),编组事件,确定路由目标,以及(可选且取决于代理)路由键。Spring Modulith 1.1 M1 开箱即用地提供了此类集成。例如,要将其与 Kafka 一起使用,您需要将相应的构件添加到项目的类路径中。

<dependency>
  <groupId>org.springframework.modulith</groupId>
  <artifactId>spring-modulith-events-api</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.modulith</groupId>
  <artifactId>spring-modulith-events-kafka</artifactId>
  <scope>runtime</scope>
</dependency>

后一个 JAR 的存在会注册一个侦听器,如上所述。要使应用程序事件能够透明地发布到代理,您可以为它添加 Spring Modulith(第一个 JAR)或 jMolecules(未显示)提供的 @Externalized 注释,如下所示。

import org.springframework.modulith.events.Externalized;

@Externalized("orders.OrderCompleted::#{customerId()}")
record OrderCompleted(OrderId orderId, CustomerId customerId) {}

注释的存在会触发该类的实例被选择用于发布。我们已将 orders.OrderCompleted 定义为路由目标。SpEL 表达式 #{customerId()} 会选择要在事件上调用的访问器方法,以生成路由键,从而触发正确的袷分(partition)分配。如果您更喜欢在代码中描述事件选择和路由,请查看如何使用 EventExternalizationConfiguration

错误场景

这一切都非常方便,我们已经优雅地解决了三个问题中的两个。但错误场景呢?如果消息发布失败了怎么办?原始业务事务已经提交,但我们现在已经丢失了内部事件发布。幸运的是,Spring Modulith 的 事件发布注册表 已经解决了这种情况。它为每个对正在发布的事件感兴趣的事务性事件侦听器创建一个注册表条目,并且仅在侦听器成功时才将该条目标记为已完成。未能将消息发送到代理会导致条目保留下来,稍后可供重试。

总结

出于性能、可靠性和一致性原因,应避免在主要业务事务中与第三方基础设施进行交互。Spring Modulith 1.1 允许通过标记事件类型进行外发并定义路由目标和键,轻松地将应用程序事件发布到消息代理。有关更多信息,请参阅 参考文档

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助您加速进步。

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看所有