透過 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 社群中所有即將舉行的活動。

檢視全部