解密 DDD:領域事件 -- 系統解耦的終極武器

1. 應用場景

假如你是訂單服務的一名研發,正在開發支付成功這個業務功能,在深度學習 DDD 後,你寫出了一組漂亮的代碼。

@Transactional
public void paySuccess(Long orderId){

    // 1. 獲取並驗證訂單聚合根有效性
    Order order = this.orderRepository.getById(orderId);
    if (order == null){
        throw new IllegalArgumentException("訂單不存在");
    }

    // 2. 修改價格
    order.paySuccess();

    // 3. 保存 Order 聚合
    this.orderRepository.save(order);

    // 4. 通知物流服務進行發貨
}

成功上線後,系統運行穩定。隨後,你又陸續接到更多需求,比如:

  1. 觸達通知:支付成功後,需要向用戶發送觸達短信,告知用戶已經完成支付;

  2. 清理購物車:用戶成功購買商品後,把該商品從購物車中移除;

  3. 確認優惠券:如果用戶購買時使用了優惠券,支付成功後調用優惠券服務標記優惠券已經被使用;

  4. 風控管理:完成支付後,調用風控系統提交訂單數據,以便對當前交易進行風險評估;

  5. …..

更多的需求還在路上,此時原本漂亮的代碼已經逐漸失控,變得有些面目全非:

@Transactional
public void paySuccess(Long orderId){

    // 1. 獲取並驗證訂單聚合根有效性
    Order order = this.orderRepository.getById(orderId);
    if (order == null){
        throw new IllegalArgumentException("訂單不存在");
    }

    // 2. 修改價格
    order.paySuccess();

    // 3. 保存 Order 聚合
    this.orderRepository.save(order);

    // 4. 通知物流服務進行發貨

    // 5. 爲用戶發生觸達短信
    // 發送觸達短信邏輯

    // 6. 清理購物車

    // 7. 使用優惠券,更新優惠券狀態

    // 8. 提交風控管理

    // 其他代碼

}

一些問題慢慢的浮現出來:

  1. 代碼極速腐化:paySuccess 代碼越來越多,想調整邏輯,需要從頭看到尾,一不小心就會出錯;

  2. 事務壓力變大:方法越來越長,事務邊界越來越大,佔用數據庫連接的時間越來越長,系統性能快速下降;

  3. 依賴越來越複雜:OrderApplicationService 實現類中,產生了很對外部依賴,比如物流、短信、購物車、優惠券、風控等;

前期這些問題你可能並不在意,直到有一天出現線上問題:

  1. 三方短信通道出現問題,影響訂單支付!

  2. 購物車服務抖動,訂單狀態仍舊是待支付!

  3. 大數據風控服務上線,訂單支付功能出現短時間不可用!

聰明的你爲了避免別人的服務到影響自己,悄悄的在每個業務調用時增加了 try-catch,但腐化仍舊在延續……

如果你也意識到這個問題,那正是引入領域事件的好時機。

2. 領域事件

領域事件是領域模型的重要組成部分,用於表示在領域中發生的一些重要的業務事情或者狀態變化,它用來捕獲領域中的一些變更,記錄事件發生時的業務狀態,並將這些數據傳輸到訂閱方,以開展後續業務操作。

領域事件有以下一些特點:

  1. 不可變性:領域事件表示已經發生的某種事實,該事實在發生後便不會改變,通常將其建模爲值對象;

  2. 解耦系統:領域事件是事件驅動的核心組成部分,用於解耦系統中的各個部分,使得系統變得更加靈活、可擴展。通過發佈訂閱模式,發佈領域事件,讓訂閱者自行訂閱,從而達到解耦的目的;

  3. 最終一致性:通過領域事件來達到最終一致性,提高系統的穩定性和性能;

領域事件分爲內部領域事件和外部領域事件,想搞清楚兩者的區別,需要先回顧下 “六邊形架構”:

  1. 內六邊形爲領域模型,承載業務邏輯,內部領域事件應用於內六邊形,主要用於服務或組件內部,在同一個服務、應用或限界上下文內實現解耦。

  2. 外六邊形爲基礎設施,承載技術複雜性,外部領域事件應用於外六邊形。用於實現跨服務、應用或限界上下文之間的通信,主要用於在微服務架構中實現解耦,或者在不同子域或限界上下文之間傳播信息。

2.1. 內部領域事件

內部領域事件的主要目標是在領域間傳播信息,以實現業務邏輯的分離和職責隔離。

內部領域事件通常使用同步或異步的方式在內存中傳播。例如,在 Java Spring 中,可以使用 ApplicationEventPublisher 和 @EventListener 實現同步或異步的內部領域事件,這些事件不會跨服務或應用傳播。

內部領域事件工作在內存中,在設計時需要注意以下幾點:

  1. 直接使用 DDD 模型,無需轉化爲 DTO:所有操作都是在內存中完成,無需考慮對象粒度問題,直接使用即可,沒有性能開銷;

  2. 包含上下文的基礎信息:通常包含事件發生的時間、事件類型、事件源和與事件相關的任何其他數據;

  3. 保持事件處理器職責單一:事件發佈者與事件處理器之間爲一對多的關係,事件處理器本身就是一個極佳的擴展點,不要爲了減少事件處理器的數量而將邏輯耦合併到同一個處理器;

  4. 錯誤處理和重試策略:爲了確保事件處理的可靠性和健壯性,在實現事件監聽器時,要考慮到可能的錯誤場景,並設計相應的異常處理和重試策略;

  5. 同步或異步處理:根據業務需求決定事件是同步還是異步處理。同步意味着在發佈事件後,事件處理器將立即執行,而發佈者將等待其完成。異步意味着發佈者將立即返回,事件處理將在另一個線程中進行。在考慮使用哪種方式時,需充分考慮資源競爭、鎖定、超時等;

Spring Event 是內部領域事件落地的一把利器,稍後進行詳解。

2.2. 外部領域事件

外部領域事件的主要目標是在跨服務或子域實現分佈式的業務邏輯和系統間解耦。

外部領域事件通常使用消息隊列(如 Rocketmq、Kafka 等)實現異步的跨服務傳播。

外部領域事件工作在消息中間件之上,在設計時需要注意以下幾點:

  1. 定製化 DTO:外部領域事件基於消息隊列進行傳播,對於龐大且數據巨大的領域對象非常不友好,同時爲了防止內部概念的外泄,無法直接使用,需要對領域事件進行自定義;

  2. 事件序列化和反序列化:設計事件的序列化和反序列化機制,以便在不同系統之間傳輸和處理。常用的序列化格式包括 JSON、XML、和二進制序列化,如 Avro、Protobuf 等,需要充分考慮消息兼容問題;

  3. 事件發佈和訂閱:選擇一個支持可靠、高性能傳輸的消息中間件。例如,Kafka、RocketMQ 等;

  4. 共享事件契約:契約包括:mq 集羣、topic、tag、Message 定義、Sharding Key 等;

  5. 錯誤處理和重試策略:和處理內部領域事件相似,需要考慮外部領域事件可能出現的錯誤,並設計相應的重試策略。特別是網絡傳輸過程中可能出現的丟失、重複或延遲問題,需要設計相應的冪等操作、消息去重和順序保證等措施;

消息中間件是 外部領域事件 落地的關鍵技術,由於篇幅原因,在此不做過多解釋。稍後會有文章進行詳解。

3. Spring  Event 機制

Spring Event 是 Spring Framework 中的一個模塊,幫助在應用程序中實現事件驅動。它主要用於組件之間同步 / 異步通信,解耦事件發佈者和事件消費者。

使用 Spring Event 包括以下步驟:

  1. 定義事件:創建一個事件類,該類封裝與特定事件相關的數據;

  2. 創建事件監聽器:定義一個或多個事件監聽器,在監聽器中,處理特定類型的事件;

  3. 發佈事件:調用 ApplicationEventPublisher 方法向外發佈事件;

在 Spring 中,事件的處理器可以通過三種方式來實現:

  1. 基於接口的事件處理:通過實現 ApplicationListener 接口並重寫 onApplicationEvent 方法來處理事件;

  2. 基於註解的事件處理:通過在方法上添加 @EventListener 或 @TransactionEventListener 註解來處理事件,可以指定事件的類型以及監聽的條件等;

  3. 基於異步事件處理:通過使用 @Async 註解來異步處理事件,可以提高應用程序的響應速度;

3.1. 基於接口的事件處理

由於與 Spring 存在強耦合,現在已經很少使用,可以直接跳過。

下面是一個基於接口的事件處理的示例代碼:

@Component
public class MyEventListener implements ApplicationListener<MyEvent> {
    @Override
    public void onApplicationEvent(MyEvent event) {
        // 處理事件
        System.out.println("Received event: " + event.getMessage());
    }
}

public class MyEvent {
    private String message;

    public MyEvent(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

@Component
public class MyEventPublisher {
    @Autowired
    private ApplicationEventPublisher eventPublisher;

    public void publishEvent(String message) {
        MyEvent event = new MyEvent(message);
        eventPublisher.publishEvent(event);
    }
}

在這個示例中,MyEvent 是一個自定義的事件類,MyEventListener 是一個實現了 ApplicationListener 接口的監聽器,用於處理 MyEvent 事件,MyEventPublisher 是用於發佈事件的類。

當應用程序調用 MyEventPublisher 的 publishEvent 方法時,會觸發一個 MyEvent 事件,MyEventListener 中的 onApplicationEvent 方法將被自動調用,從而處理這個事件。

3.2. 基於註解的事件處理

Spring 提供 @EventListener 和 @TransactionListener 兩個註解以簡化對事件的處理。

3.2.1. @EventListener

Spring 的 EventListener 監聽器是一種相對於傳統的事件監聽方式更爲簡潔和靈活的事件機制。與傳統的事件機制不同,EventListener 不需要顯示地繼承特定的事件接口,而是使用註解標識需要監聽的事件類型,然後通過一個單獨的監聽器類處理所有類型的事件。

相比之下 EventListener 的優勢主要有以下幾點:

  1. 更加靈活:EventListener 不依賴於任何特定的事件接口,從而使得事件處理更加靈活,可以監聽和處理任意類型的事件;

  2. 更加簡潔:相比傳統的事件監聽方式,使用 EventListener 可以避免一系列繁瑣的接口定義和實現,簡化了代碼結構,提升開發效率;

  3. 更加松耦合:EventListener 將事件發佈方和事件處理方分離,遵循松耦合的設計原則,提高了代碼的可維護性和擴展性;

  4. 更加可測試:由於 EventListener 可以監聽和處理任意類型的事件,可以通過單元測試驗證其功能是否正確,從而提高了測試的可靠性;

以下是一個簡單的例子:

@Component
public class MyEventListener{

    @EventListener
    public void onApplicationEvent(MyEvent event) {
        // 處理事件
        System.out.println("Received event: " + event.getMessage());
    }
}

public class MyEvent {
    private String message;

    public MyEvent(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

@Component
public class MyEventPublisher {

    @Autowired
    private ApplicationEventPublisher eventPublisher;

    public void publishEvent(String message) {
        MyEvent event = new MyEvent(message);
        eventPublisher.publishEvent(event);
    }
}

相比基於接口的事件處理,EventListener 是一種更加簡潔、靈活、松耦合、可測試的事件機制,能夠有效地降低開發的複雜度,提高開發效率。

3.2.2. @TransactionEventListener

在 Spring 中,TransactionEventListner 和 EventListner 都是用於處理事件的接口。不同之處在於

  1. TransactionEventListner 是在事務提交後纔會觸發;

  2. 而 EventListner 則是在事件發佈後就會觸發;

具體來說,在使用 Spring 的聲明式事務時,可以在事務提交後觸發某些事件。這就是 TransactionEventListner 的應用場景。而 EventListner 則不涉及事務,可以用於在事件發佈後觸發一些操作。

下面是一個簡單的示例,演示瞭如何使用 TransactionEventListner 和 EventListner:

@Component
public class MyEventListener {

    @EventListener
    public void handleMyEvent(MyEvent event) {
        // 處理 MyEvent
    }

    @TransactionalEventListener
    public void handleMyTransactionalEvent(MyTransactionalEvent event) {
        // 處理 MyTransactionalEvent
    }
}

@Service
public class MyService {

    @Autowired
    private ApplicationEventPublisher eventPublisher;

    @Autowired
    private MyRepository myRepository;

    @Transactional
    public void doSomething() {
        // 做一些事情
        MyEntity entity = myRepository.findById(1L);
        // 發佈事件
        eventPublisher.publishEvent(new MyEvent(this, entity));
        // 發佈事務事件
        eventPublisher.publishEvent(new MyTransactionalEvent(this, entity));
    }
}

在這個例子中,MyEventListener 類定義了兩個方法,handleMyEvent 和 handleMyTransactionalEvent,分別處理 MyEvent 和 MyTransactionalEvent 事件。其中,handleMyTransactionalEvent 方法用 @TransactionalEventListener 註解標記,表示它只會在事務提交後觸發。

MyService 類中的 doSomething 方法使用 ApplicationEventPublisher 來發布事件。注意,它發佈了兩種不同類型的事件:MyEvent 和 MyTransactionalEvent。這兩個事件會分別觸發 MyEventListener 中的對應方法。

總的來說,Spring 的事件機制非常靈活,可以方便地擴展應用程序的功能。TransactionEventListner 和 EventListner 這兩個接口的應用場景有所不同,可以根據實際需求選擇使用。

3.3. 基於異步事件處理

@Async 是 Spring 框架中的一個註解,用於將一個方法標記爲異步執行。使用該註解,Spring 將自動爲該方法創建一個新線程,使其在後臺異步執行,不會阻塞主線程的執行。

在實際應用中,使用 @Async 可以大大提升應用的併發處理能力,使得系統能夠更快地響應用戶請求,提高系統的吞吐量。

@Async 和 @EventListener 或 @TransactionEventListener 註解在一起使用時,會產生異步的事件處理器。使用這種組合的方式,事件處理器會在單獨的線程池中執行,以避免阻塞主線程。這種方式在需要處理大量事件或者事件處理器耗時較長的情況下非常有用,可以有效提升應用的性能和可伸縮性。同時,Spring 框架對這種方式也提供了完善的支持,可以方便地使用這種方式來實現異步事件處理。

下面是一個簡單的示例代碼,演示瞭如何在 Spring 中使用 @Async 和 @EventListener 一起實現異步事件處理:

@Component
public class ExampleEventListener {

    @Async
    @EventListener
    public void handleExampleEvent(ExampleEvent event) {
        // 在新的線程中執行異步邏輯
        // ...
    }
}

在這個示例中,ExampleEventListener 類中的 handleExampleEvent 方法使用了 @Async 和 @EventListener 註解,表示這個方法是一個異步事件監聽器。當一個 ExampleEvent 事件被觸發時,這個方法會被異步地執行。在這個方法中,可以執行任何異步的邏輯處理,比如向隊列發送消息、調用其他服務等。

備註:在使用 @Async 時,需要根據業務場景對線程池進行自定義,以免出現資源不夠的情況(Spring 默認使用單線程處理 @Async 異步任務)

4. Spring Event 應用場景分析

綜上所述,當領域事件發出來之後,不同的註解會產生不同的行爲,簡單彙總如下:

jocvvp

4.1. @EventListener

特點:

  1. 順序執行。調用 publish(Event) 後,自動觸發對 @EventListner 註釋方法的調用

  2. 同步執行。使用主線程執行,方法拋出異常會中斷調用鏈路,會觸發事務的迴歸

應用場景:

  1. 事務消息表。在同一事務中完成對業務數據和消息表的修改

  2. 業務驗證。對業務對象進行最後一次驗證,如果驗證不通過直接拋出異常中斷數據庫事務

  3. 業務插件。在當前線程和事務中執行插件完成業務擴展

4.2. @TransactionEventListener

特點:

  1. 事務提交後執行。調用 publish(Event) 時,只是向上下文中註冊了一個回調器,並不會立即執行;只有在事務提交後,纔會觸發 @TransactionEventListner 註釋方法的執行

  2. 同步執行。使用主線程執行,方法拋出異常會中斷調用鏈路,但不會迴歸事務(事務已提交,沒有辦法進行迴歸)

應用場景:

  1. 數據同步。事務提交後,將變更同步到 ES 或 Cache

  2. 記錄審計日誌。只有在業務變更成功更新到數據庫時才進行記錄

備註:@TransactionEventLisnter 必須在事務上下文中,脫離上下文,調用不會生效

4.3. @EventListener + @Async

特點:

  1. 順序執行。調用 publish(Event) 後,自動觸發對 @EventListner 註釋方法的調用

  2. 異步執行。使用獨立的線程池執行任務,方法拋出異常對主流程沒有任何影響

應用場景:

  1. 記日誌明細日誌,輔助排查問題

4.4. @TransactionEventListener + @Async

特點:

  1. 事務提交後執行。調用 publish(Event) 時,只是向上下文中註冊了一個回調器,並不會立即執行;只有在事務提交後,纔會觸發對 @TransactionEventListner 註釋方法的調用

  2. 異步執行。使用獨立的線程池執行任務,方法拋出異常對主流程沒有任何影響

應用場景:
異步處理。記錄操作日誌,異步保存數據等

備註:@TransactionEventLisnter 必須在事務上下文中,脫離上下文,調用不會生效

5. 小結

領域事件是系統中的解耦利器,包括:

  1. 內部事件 完成 領域模型內各組件間的解耦;

  2. 外部事件 完成 領域服務間的解耦;

Spring Event 是實現內部領域事件解耦的利器,基於 事件監聽註解 和 同步 / 異步 兩組註解的組合爲不同的應用場景提供不同的支持。

Vg9T6Z

外部領域事件 強依賴於消息中間件的使用,稍後會有文章進行詳解。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/YhQvPzbX3q47A5mZ3oUIyA