領域事件:解耦微服務的關鍵
領域事件
領域事件在領域模型裏佔據着舉足輕重的地位,它用於表徵領域內發生的各類事件。一旦某個領域事件發生,便會引發後續一系列的業務操作。這一機制不僅能夠實現業務解耦,還對構建完整的業務閉環大有裨益。
舉個例子,在業務流程中,領域事件可能體現爲其中的某一個具體步驟。以投保業務爲例,當繳費環節完成後,就會觸發將投保單轉換爲保單的動作;在定時批處理場景下,也會涉及領域事件。比如在批處理生成季繳保費通知單的過程中,就會觸發發送繳費郵件通知的操作;此外,領域事件還可能表現爲一個事件發生後所引發的後續動作。例如,當密碼連續輸錯三次時,系統便會觸發鎖定賬戶的動作 。
那如何識別領域事件呢?
領域事件與之前所講的定義緊密相連。在進行用戶旅程或場景分析時,我們需留意業務人員、需求人員或領域專家話語中的一些關鍵詞,像 “如果發生……,則……”“當做完…… 的時候,請通知……”“發生…… 時,則……” 等。在這些場景裏,若某個事件發生後會觸發後續操作,那麼這個事件極有可能就是領域事件。
接下來,我們探討一下領域事件爲何採用最終一致性,而非傳統 SOA 的直接調用方式。在邊界之外使用最終一致性,一次事務最多隻能更改一個聚合的狀態。要是一次業務操作涉及多個聚合狀態的更改,就應該採用領域事件的最終一致性。
領域事件驅動設計能夠切斷領域模型之間的強依賴關係。事件發佈完成後,發佈方無需關注後續訂閱方事件處理是否成功,如此便能實現領域模型的解耦,維護領域模型的獨立性和數據的一致性。當領域模型映射到微服務系統架構時,領域事件可以解耦微服務,微服務之間的數據無需強一致性,而是基於事件的最終一致性。
在具體的業務場景中,我們會發現領域事件的發生情況有所不同,有的發生在微服務內的聚合之間,有的發生在微服務之間,還有兩者皆有的情況。一般來說,跨微服務的領域事件處理更爲常見。所以在微服務設計時,針對不同的領域事件,處理方式也會有所差異。
- 微服務內的領域事件
在微服務內部,當領域事件發生在聚合之間時,處理流程如下:領域事件發生後,首先會完成事件實體的構建以及事件數據的持久化操作。接着,發佈方聚合會將事件發佈至事件總線,訂閱方則接收事件數據,進而完成後續的業務操作。
大部分微服務內的事件集成,都發生在同一個進程之中。由於進程自身具備良好的事務控制能力,所以在這種情況下,不一定非要引入消息中間件。然而,倘若一個事件同時涉及多個聚合的更新,依據 DDD 中 “一次事務只更新一個聚合” 的原則,此時就需要考慮是否引入事件總線。不過,微服務內的事件總線可能會提升開發的複雜程度,所以在實際應用中,需要綜合考量應用的複雜度與收益情況,做出合理的決策 。
在微服務內的應用服務方面,對於實時性和數據一致性要求較高的場景,可通過跨聚合的服務編排與組合,以服務調用的方式實現跨聚合的訪問。在這個過程中,會運用分佈式事務,以此確保發佈方和訂閱方的數據能夠同時成功更新。
- 微服務之間的領域事件
在不同的限界上下文或領域模型之間,跨微服務的領域事件能夠實現業務協作。其核心目標在於解耦微服務,緩解微服務之間實時服務訪問所面臨的壓力。在實際情況中,領域事件發生在微服務之間的場景較爲常見,相應的事件處理機制也更爲複雜。
跨微服務的事件能夠促使業務流程或者數據在不同的子域或微服務之間直接流轉。對於跨微服務的事件機制,需要從整體上綜合考慮多個方面,其中包括事件構建、發佈與訂閱、事件數據持久化以及消息中間件等。值得注意的是,在進行事件數據持久化時,甚至可能需要引入分佈式事務機制。
除了通過領域事件進行協作外,微服務之間的訪問也可以採用應用服務直接調用的方式,以此實現數據和服務的實時訪問。不過,這種方式存在一定弊端,當涉及跨微服務的數據同時變更時,就需要引入分佈式事務來保障數據的一致性。然而,分佈式事務機制不僅會對系統性能產生影響,還會增加微服務之間的耦合度。因此,在實際應用中,我們應儘可能避免使用分佈式事務 。
領域事件相關案例
我來給你介紹一個保險承保業務過程中有關領域事件的案例。
一個保單的生成,經歷了很多子域、業務狀態變更和跨微服務業務數據的傳遞。這個過程會產生很多的領域事件,這些領域事件促成了保險業務數據、對象在不同的微服務和子域之間的流轉和角色轉換。
在下面這張圖中,我列出了幾個關鍵流程,用來說明如何用領域事件驅動設計來驅動承保業務流程。
事件起點:客戶購買保險 - 業務人員完成保單錄入 - 生成投保單 - 啓動繳費動作。
在投保業務的微服務架構中,領域事件驅動着業務流程的流轉。
首先,投保微服務生成繳費通知單後,會發布第一個領域事件 ——“繳費通知單已生成”,並將繳費通知單數據發送至消息中間件。收款微服務訂閱該事件,進而完成繳費操作。當繳費通知單生成這一事件相關的操作全部完成後,此領域事件結束。
接着,收款微服務在繳費完成後,搖身一變成爲發佈方,發佈第二個領域事件 ——“繳費已完成”,同時將繳費數據發佈到消息中間件。此時,投保微服務角色轉換爲訂閱方。投保微服務在收到繳費信息並確認繳費完成後,執行投保單轉成保單的操作。隨着繳費完成相關操作的結束,該領域事件也隨之畫上句號。
然後,投保微服務在投保單成功轉爲保單後,發佈第三個領域事件 ——“保單已生成”,並把保單數據發佈到消息中間件。保單微服務接收到保單數據後,進行保單數據保存操作。保單生成相關操作結束,該領域事件也結束。
最後,保單微服務完成保單數據保存後,後續還會觸發一系列領域事件。這些事件以併發的方式,通過消息中間件將保單數據發送到佣金、收付費、再保等微服務,直至流轉到財務微服務,完成保單後續的所有業務流程,這裏就不再詳細展開。
綜上所述,藉助領域事件驅動的異步化機制,業務流程和數據能夠在各個不同的微服務之間順暢流轉。這種方式實現了微服務的解耦,有效減輕了微服務之間服務調用的壓力,最終提升了用戶體驗。
領域事件總體架構
領域事件的執行需要一系列的組件和技術來支撐。我們來看一下這個領域事件總體技術架構圖,領域事件處理包括:事件構建和發佈、事件數據持久化、事件總線、消息中間件、事件接收和處理等。下面我們逐一講一下。
在領域事件的處理過程中,事件的構建和發佈涉及多個重要方面。
事件的基本屬性是事件構建的重要組成部分,它至少包含事件唯一標識、發生時間、事件類型和事件源。其中,事件唯一標識必須保證全局唯一,這是確保事件能夠在多個限界上下文中準確、無歧義傳遞的關鍵。這些基本屬性主要用於記錄事件自身的特徵以及事件發生的背景數據。
而在事件中,還有一項至關重要的內容,即業務屬性。業務屬性用於記錄事件發生瞬間的業務數據,這些數據會隨着事件一同傳輸到訂閱方,從而爲訂閱方開展下一步業務操作提供必要的信息支持。
事件基本屬性和業務屬性共同構成了事件實體。值得注意的是,事件實體依賴於聚合根。當領域事件發生後,事件中的業務數據便不再進行修改,基於此,業務數據可以採用序列化值對象的形式進行保存。這種存儲格式在消息中間件中具有易於解析和獲取的優勢。
爲了保證事件結構的統一,通常會創建事件基類 DomainEvent(可參考下圖)。通過這個基類,子類能夠根據實際需求擴充屬性和方法。由於事件本身所涉及的業務行爲相對較少,所以其實現方法一般較爲簡單。
事件發佈之前需要先構建事件實體並持久化。事件發佈的方式有很多種,你可以通過應用服務或者領域服務發佈到事件總線或者消息中間件,也可以從事件表中利用定時程序或數據庫日誌捕獲技術獲取增量事件數據,發佈到消息中間件。
- 事件數據持久化
事件數據持久化具備多種重要用途。一方面,它能夠爲系統之間的數據對賬提供有力支持;另一方面,也有助於實現對發佈方和訂閱方事件數據的審計。此外,當遭遇消息中間件故障、訂閱方系統宕機或者網絡中斷等情況時,在問題得到妥善解決後,藉助事件數據持久化可以繼續推進後續業務流轉,從而有效保障數據的一致性。
在進行事件數據持久化時,存在兩種可供選擇的方案,在實際實施過程中,可依據具體業務場景靈活抉擇。
第一種方案是將事件數據持久化到本地業務數據庫的事件表中。這種方式能夠利用本地事務來確保業務數據和事件數據的一致性。
第二種方案是將事件數據持久化到共享的事件數據庫中。不過,需要特別留意的是,業務數據庫和事件數據庫並非處於同一個數據庫中,這就導致它們的數據持久化操作會涉及跨數據庫的情況。所以,爲了保證業務數據和事件數據的強一致性,就需要引入分佈式事務機制。但這種機制也存在一定弊端,它會在一定程度上影響系統的性能 。
- 事件總線 (EventBus)
事件總線是實現微服務內聚合之間領域事件的關鍵組件,在微服務架構中扮演着重要角色。它主要負責提供事件分發和接收等服務,確保領域事件能夠在不同聚合之間順利傳遞。
事件總線採用的是進程內模型,在微服務內的聚合之間發揮作用時,它會遍歷訂閱者列表,按照同步或異步的模式進行數據傳遞。這種工作模式使得事件總線能夠根據不同的業務需求和系統環境,靈活地選擇最合適的數據傳遞方式,從而保障系統的高效運行。
在事件分發流程方面,事件總線有着明確的規則。當面對微服務內的訂閱者(即其他聚合)時,事件總線會直接將事件分發到指定的訂閱者,確保數據能夠快速、準確地到達目標位置。而當遇到微服務外的訂閱者時,事件總線會先將事件數據保存到事件庫(表)中,然後通過異步的方式將數據發送到消息中間件,以此實現跨微服務的事件傳遞。如果同時存在微服務內和外的訂閱者,事件總線則會先將事件分發到內部訂閱者,接着把事件消息保存到事件庫(表),最後再異步發送到消息中間件。通過這樣的流程,事件總線既保證了內部聚合能夠及時接收到事件,又兼顧了外部訂閱者的需求,使得整個微服務架構下的事件傳遞更加有序、高效。
- 消息中間件
跨微服務的領域事件大多會用到消息中間件,實現跨微服務的事件發佈和訂閱。消息中間件的產品非常成熟,市場上可選的技術也非常多,比如 Kafka,RabbitMQ 等。
- 事件接收和處理
微服務訂閱方在應用層採用監聽機制,接收消息隊列中的事件數據,完成事件數據的持久化後,就可以開始進一步的業務處理。領域事件處理可在領域服務中實現。
領域事件運行機制相關案例
這裏我用承保業務流程的繳費通知單事件,來給你解釋一下領域事件的運行機制。這個領域事件發生在投保和收款微服務之間。發生的領域事件是:繳費通知單已生成。下一步的業務操作是:繳費。
事件起點:出單員生成投保單,覈保通過後,發起生成繳費通知單的操作。
在投保業務流程中,投保微服務的應用服務率先啓動關鍵操作。它調用聚合中的領域服務 createPaymentNotice 來創建繳費通知單,同時調用 createPaymentNoticeEvent 創建繳費通知單事件。這裏的繳費通知單事件類 PaymentNoticeEvent 是繼承自基類 DomainEvent 。
完成創建操作後,藉助倉儲服務將繳費通知單相關的業務數據和事件數據進行持久化。爲避免分佈式事務帶來的複雜性,這些數據都被存儲在本地投保微服務數據庫中。
隨後,通過數據庫日誌捕獲技術或者設置定時程序,從數據庫的事件表中提取事件增量數據,並將其發佈至消息中間件。需要說明的是,事件發佈操作也可由應用服務或者領域服務來完成。
收款微服務在應用層訂閱消息中間件中的繳費通知單事件消息主題。一旦監聽並獲取到事件數據,收款微服務的應用服務便調用領域層的領域服務,將事件數據持久化到本地數據庫。
最後,收款微服務調用領域層的領域服務 PayPremium,順利完成繳費操作,至此本次事件的主要流程結束。
值得注意的是,繳費完成後,後續流程中的微服務還會觸發一系列新的領域事件,諸如 “繳費已完成”“保單已保存” 等。這些後續事件的處理機制,大體上與上述業務流程的處理方式相似。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/HILiHuL1pDUhC1mwQ7KYVg