領先一步
VMware 提供培訓和認證,助您加速進步。
瞭解更多事務性服務方法在 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;
}
}
不幸的是,這種方法存在各種問題。
completeOrder(…) 方法現在更容易受到更多基礎設施問題的影響。無法訪問代理會導致事務回滾,並阻止訂單完成。由於下游基礎設施問題,我們的系統可能在技術上可用,但完全無法執行任何有用的操作。解決這些問題的一個常見模式是從服務中釋出應用程式事件,乍一看,這與我們之前概述的並沒有太大區別。
@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 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 允許透過標記事件型別進行外發並定義路由目標和鍵,輕鬆地將應用程式事件釋出到訊息代理。有關更多資訊,請參閱 參考文件。