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

檢視所有