使用 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()} 选择将在事件上调用的访问器方法来生成路由键,从而触发正确的分区分配。如果您更喜欢在代码中描述事件选择和路由,请查阅如何使用 EventExternalizationConfiguration

错误场景

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

总结

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

订阅 Spring 邮件列表

通过 Spring 邮件列表保持联系

订阅

提升自我

VMware 提供培训和认证,助力您快速提升。

了解更多

获取支持

Tanzu Spring 通过一个简单的订阅提供对 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

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

查看全部