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

檢視全部