使用 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 的 事件发布注册表 已经解决了这种情况。它为每个对要发布的事件感兴趣的事务性事件侦听器创建一个注册表条目,并且仅当侦听器成功时才会将该条目标记为已完成。无法将消息发送到代理会导致该条目保留下来,并稍后重新提交尝试。

总结

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

获取 Spring 时事通讯

与 Spring 时事通讯保持联系

订阅

领先一步

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

了解更多

获取支持

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

了解更多

即将举行的活动

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

查看全部