天天看點

《微服務架構設計模式》讀書筆記 | 第6章 使用事件溯源開發業務邏輯 #yyds幹貨盤點# 前言1. 使用事件溯源開發業務邏輯概述2. 實作事件存儲庫3. 同時使用Saga和事件溯源4. 本章小結最後

(第6章 使用事件溯源開發業務邏輯)

前言

事件溯源是一種以事件為中心的編寫業務邏輯和持久化領域對象的方法。事件溯源可以消除一些可能的程式設計錯誤,因為這項技術可以保證在建立或更新聚合時一定會釋出事件。

這是一本關于微服務架構設計方面的書,這是本人閱讀的學習筆記。下面對一些符号做些說明:

()為補充,一般是書本裡的内容;

[]符号為筆者筆注;

1. 使用事件溯源開發業務邏輯概述

事件溯源模式:使用一系列便是狀态更改的領域事件來持久化聚合。

1.1 傳統持久化技術的問題

  • 對象與關系的“阻抗失調”:關系型資料的表格結構模式,與領域模型及其複雜關系的圖狀結構之間,存在基本的概念不比對的問題;
  • 缺乏聚合曆史:聚合更新後,其先前的狀态将丢失;
  • 實施更新功能将非常繁瑣且容易出錯:耗時,負責記錄審計日志的代碼可能會和業務邏輯代碼發生偏離;
  • 事件釋出淩駕于業務邏輯之上:無法把自動釋出消息作為更新資料事務的一部分;

1.2 事件溯源通過事件來持久化聚合

事件溯源采用基于領域事件的概念來實作聚合的持久化;它将每個聚合持久化為資料庫中的一系列事件,稱為事件存儲。
《微服務架構設計模式》讀書筆記 | 第6章 使用事件溯源開發業務邏輯 #yyds幹貨盤點# 前言1. 使用事件溯源開發業務邏輯概述2. 實作事件存儲庫3. 同時使用Saga和事件溯源4. 本章小結最後

圖解:

  • 事件溯源不是将每個Order作為一行存儲在ORDER表中,而是将每個Order聚合持久化為EVENTS表中的一行或多行;
  • 應用程式建立或更新聚合時,它會将聚合發出的事件插入到EVENTS表中;
  • 應用程式通過從事件存儲中檢索并重放事件來加載聚合(如Eventuate Client架構),加載聚合的步驟:
    • 加載聚合的事件;
    • 使用其預設構造函數建立聚合執行個體;
    • 調用apply()方法周遊事件;
  • 事件溯源通過加載事件和重放事件來重建聚合的記憶體狀态;

1.3 事件溯源對領域事件提出的新需求

  • 事件代表狀态的改變;
  • 聚合方法都和事件相關;

1.4 事件代表狀态的改變

  • 在事件溯源情況下,聚合主要決定事件及其結構;
  • 包括建立在内的每一個聚合狀态變化,都由領域事件表示;
  • 每當聚合的狀态發生改變時,它必須發出一個事件;
  • 事件中必須包含聚合執行狀态變化所需的資料;
  • 聚合的狀态由構成聚合對象的字段值組成;
《微服務架構設計模式》讀書筆記 | 第6章 使用事件溯源開發業務邏輯 #yyds幹貨盤點# 前言1. 使用事件溯源開發業務邏輯概述2. 實作事件存儲庫3. 同時使用Saga和事件溯源4. 本章小結最後

1.5 聚合方法都和事件相關;

  • 基于事件溯源的應用程式中的指令方法通過生成事件來處理對聚合更新的請求;
  • 調用聚合指令方法的結果是一系列事件,表示必須進行的狀态更改;
《微服務架構設計模式》讀書筆記 | 第6章 使用事件溯源開發業務邏輯 #yyds幹貨盤點# 前言1. 使用事件溯源開發業務邏輯概述2. 實作事件存儲庫3. 同時使用Saga和事件溯源4. 本章小結最後
  • 生成事件并應用(apply)事件的做法将導緻對業務邏輯的重構;事件溯源将指令方法重構為兩個或更多個方法;
    • 第一個方法

      process()

      接收指令對象參數,該參數表示具體的請求,并确定需要自行哪些狀态更改;它驗證指令對象的參數,并且在不更改聚合狀态的情況下,傳回表示狀态更改的事件清單;如果無法執行該指令,則此方法通常會引發異常;
    • 其他方法

      apply()

      都将特定事件類型作為參數來更新聚合;這些方法與聚合産生的事件類型一一對應;重要的是要注意執行這些方法不會出現失敗,因為這些事件代表了一個已經發生的狀态變化;每個方法都會根據事件更新聚合;
    • 一個例子如下:
《微服務架構設計模式》讀書筆記 | 第6章 使用事件溯源開發業務邏輯 #yyds幹貨盤點# 前言1. 使用事件溯源開發業務邏輯概述2. 實作事件存儲庫3. 同時使用Saga和事件溯源4. 本章小結最後

圖解:

  • reviseOrder()

    方法被

    process()

    方法和

    apply()

    方法替代;
  • process()

    方法将ReviseOrder指令作為參數;
  • process()

    方法要麼傳回OrderRevisionProposed事件,要麼抛出異常;
    • 如時間太晚已不能修改訂單或建議訂單修訂不滿足訂單最小值的時候;
  • OrderRevisionProposed事件的

    apply()

    方法将Order的狀态更改為REVISION_PENDING;

1.6 建立與更新聚合的步驟

建立聚合的步驟:

  1. 使用聚合的預設構造函數執行個體化聚合根;
  2. 調用process()以生成新事件;
  3. 周遊新生成的事件并調用apply()來更新聚合的狀态;
  4. 将新事件儲存在事件存儲庫中;

更新聚合的步驟:

  1. 從事件存儲庫加載聚合事件;
  2. 使用其預設構造函數執行個體化聚合根;
  3. 周遊加載的事件,并在聚合根上調用apply()方法;
  4. 調用其process()方法以生成新事件;
  5. 周遊新生成的事件并調用appply()來更新聚合的狀态;
  6. 将新事件儲存在事件儲存庫中;

1.7 基于事件溯源的Order聚合

《微服務架構設計模式》讀書筆記 | 第6章 使用事件溯源開發業務邏輯 #yyds幹貨盤點# 前言1. 使用事件溯源開發業務邏輯概述2. 實作事件存儲庫3. 同時使用Saga和事件溯源4. 本章小結最後
  • 業務邏輯通過指令來實作,這些指令發出事件并應用那些更新其狀态的事件;
  • 建立或更新基于JPA的聚合的每個方法,如

    createOrder()

    reviseOrder()

    ,在事件溯源版本中都由

    process()

    apply()

    方法替代;
《微服務架構設計模式》讀書筆記 | 第6章 使用事件溯源開發業務邏輯 #yyds幹貨盤點# 前言1. 使用事件溯源開發業務邏輯概述2. 實作事件存儲庫3. 同時使用Saga和事件溯源4. 本章小結最後
  • 基于JPA聚合的修改訂單業務邏輯由三個方法組成:

    reviseOrder()

    confirmRevision()

    rejectRevision()

  • 事件溯源版本使用三個

    process()

    方法和一些

    apply()

    方法替代這三個的方法;

1.8 使用樂觀鎖處理并發更新

指兩個或多個請求同時更新同一聚合的情況;
  • 樂觀鎖通常使用版本列(映射到VERSION列)來檢測聚合自讀取以來是否已更改;
  • 每當更新聚合時,VERSION列的值會增加;
  • 兩個有兩個事物讀取相同的聚合,第一個成功,第二個不成功;因為版本号已更改;

1.9 事件溯源和釋出事件

  • 使用輪詢釋出事件;
    • 如下圖所示:
      《微服務架構設計模式》讀書筆記 | 第6章 使用事件溯源開發業務邏輯 #yyds幹貨盤點# 前言1. 使用事件溯源開發業務邏輯概述2. 實作事件存儲庫3. 同時使用Saga和事件溯源4. 本章小結最後
  • 使用事務日志拖尾技術來可靠地釋出事件;
    • 本篇第2點詳解;

1.10 使用快照提升性能

長生命周期的聚合可能會有大量事件;随時間推移,加載和重放這些事件會變得越來越低效;常見解決方法是定期持久儲存聚合狀态的快照;

《微服務架構設計模式》讀書筆記 | 第6章 使用事件溯源開發業務邏輯 #yyds幹貨盤點# 前言1. 使用事件溯源開發業務邏輯概述2. 實作事件存儲庫3. 同時使用Saga和事件溯源4. 本章小結最後

1.11 幂等方式的消息處理

使用相同的消息多次安全地調用消息接收方,則消息接收方是幂等的;具體實作方式取決于事件儲存庫是關系型資料庫還是NoSQL資料庫;
  • 基于關系型資料庫事件儲存庫的幂等消息處理;
    • 可以将消息ID插入PROCESSED_MESSAGES表,作為插入EVENTS表的事件的事務的一部分 [相同消息被接受時,若資料庫中已經存在ID,則忽略該消息請求];
  • 基于非關系型資料庫事件儲存庫的幂等消息處理;
    • 往往功能有限,需要使用不同的機制來實作幂等消息處理;
    • 消息接收方必須以某種原子化的方式同時完成事件持久化和記錄消息ID;
    • 解決方案:消息消費者把消息的ID存儲在處理它時生成的事件中,它通過驗證聚合的所有事件中是否包含該消息ID來做重複驗證;
    • 該解決方案的問題:一些消息的處理可能不會生成任何事件;
      • 一個解決方案:始終釋出事件;如果聚合不發出事件,則應用程式僅儲存記錄消息ID的僞事件;事件接收方必須忽略這些僞事件;

1.12 領域事件的演化

事件溯源應用程式的結構分三個層次:

  • 由一個或多個聚合組成;
  • 定義每個聚合發出的事件;
  • 定義事件的結構;

每個級别可能發生的不同類型的更改:

《微服務架構設計模式》讀書筆記 | 第6章 使用事件溯源開發業務邏輯 #yyds幹貨盤點# 前言1. 使用事件溯源開發業務邏輯概述2. 實作事件存儲庫3. 同時使用Saga和事件溯源4. 本章小結最後
  • 服務的領域模型随着時間的推移而發展,這些變化會自然發生;
  • 不向後相容段更改都需要更改該事件類型的消費者;

通過向上轉換(Upcasting)來管理結構的變化:

  • 事件溯源架構不是将事件遷移到新的版本,而是在從事件存儲庫加載事件時執行轉換;
  • 通常用稱為“向上轉換”的元件将各個事件從舊版本更新為更新的版本;

1.13 事件溯源的好處與弊端

好處:

  • 可靠地釋出領域事件;
  • 保留聚合的曆史;
  • 最大限度地避免對象與關系的“阻抗失調”問題(持久化事件而不是聚合本身);
  • 為開發者提供一個“時光機”;

弊端:

  • 這類程式設計模式有一定的學習曲線;
  • 基于消息傳遞的應用程式的複雜性;
    • 指處理非等幂事件時;
    • 解決方法:為每個事件配置設定單調遞增的ID;
  • 處理事件的演化有一定難度;
    • 指事件和快照的結構随時間推移變得臃腫;
    • 解決方法:從事件存儲庫加載事件時,将事件更新到最新版本;即将事件版本處理與聚合的代碼分開,簡化聚合;
  • 删除資料存在一定難度;
    • 歐洲的GDPR給予使用者對其資料的擦除權,而使用者資訊可能嵌入在事件結構中,如郵箱作為聚合的主鍵,應用程式必須在不删除事件的情況下清除特定使用者資訊;
    • 解決方法:使用加密密鑰;使用假名技術(如:UUID令牌代替電子郵箱作為聚合ID);
  • 查詢事件存儲庫非常有挑戰性;
    • 指可能會使用嵌套的更為複雜的且可能低效的查詢;
    • 解決方法:參考《第7章 CQRS方法實作查詢》;

2. 實作事件存儲庫

使用事件溯源的應用程式将事件存儲在事件存儲庫中;事件存儲庫是資料庫和消息代理功能的組合;它表現為資料庫和消息代理;
  • 實作事件存儲庫有多種方法,一種是實作自己的事件存儲庫和事件溯源代碼架構;另一種是使用專用事件存儲庫;
  • 專用事件存儲庫通常提供豐富的功能集、更好的性能和可擴充性;
  • 如:Event Store、Lagom、Axon、Eventuate SaaS;

2.1 Eventuate Local事件存儲庫的工作原理

Eventuate Local的事件資料庫結構:

  • events:存儲事件(最核心);
    • 與本篇1.2點的圖類似;
  • entities:每個實體一行;
    • 儲存每個實體的目前版本;用于實作樂觀鎖;
  • snapshots:存儲快照;
    • 儲存每個實體的快照;
    • 其支援find()、create()、update()三個操作;

通過訂閱Eventuate Local的事件代理接受事件:

  • 服務通過訂閱事件代理來使用事件,事件代理具有每個聚合類型的主題;
  • 主題是分區的消息通道,使接收方能夠在保持消息排序的同時進行水準擴充;

Eventuate Local的事件中繼把事件從資料庫傳播到消息代理:

  • 事件中繼将插入事件資料庫的事件傳播到事件代理;
  • 它盡可能使用事務日志拖尾,或輪詢其他資料庫;
  • 事件部署為獨立程序;

2.2 針對Java語言的Eventuate Client架構提供的主要類和接口

Eventuate Client架構使開發人員能夠使用Eventuate Local事件存儲庫編寫基于事件溯源的應用程式;它為開發基于事件溯源的聚合、服務和事件處理程式提供了架構基礎;
《微服務架構設計模式》讀書筆記 | 第6章 使用事件溯源開發業務邏輯 #yyds幹貨盤點# 前言1. 使用事件溯源開發業務邏輯概述2. 實作事件存儲庫3. 同時使用Saga和事件溯源4. 本章小結最後

圖解:

  • 通過ReflectiveMutableCommandProcessingAggregate類定義聚合;
    • 該類是聚合的基類,是一個泛型類;
    • 有兩個類型參數:具體的聚合類、聚合指令類的超類;
    • 使用反射将指令和事件分别分派給process()和apply()方法;
  • 定義聚合指令;
    • 聚合的指令類必須擴充特定于聚合的基接口,該接口本身必須擴充Command接口;
    • 如:Order聚合的指令擴充了Ordercommand;
  • 定義領域事件;
    • 聚合的事件類必須擴充Event接口,這是一個沒有方法的辨別接口;
  • 使用AggregateRepository類建立、查找和更新聚合;
    • 該類是一個泛型類,它接收的參數是聚合類和聚合的基指令類;
    • 提供三種重載方法:save()建立聚合、find()查找聚合、update()更新聚合;
    • 主要由服務使用,在服務響應外部請求時建立和更新聚合;
  • 訂閱領域事件;
    • Eventuate Client架構還提供了用于編寫事件處理程式的API,如:
    • @EventSubscriber

      注解指定持久化訂閱方的ID;
    • @EventHandlerMethod

      注解将creditReserved()方法辨別為事件處理程式;

3. 同時使用Saga和事件溯源

事件溯源可以輕松使用基于協同式的Saga;将事件溯源的業務邏輯與基于編排的Saga相結合更具挑戰性;

3.1 使用事件溯源實作協同式Saga

  • 事件溯源的事件驅動屬性使得實作基于協同式的Saga非常簡單;
  • 當聚合被更新時,它會發出一個事件;不同聚合的事件處理程式可以接受該事件,并更新該聚合;事件溯源架構自動使每個事件處理程式具有幂等性;
  • 事件溯源代碼提供了Saga所需的機制,包括基于消息傳遞的程序間通信、消息去重,以及原子化狀态更新和消息發送;
  • 弊端:事件展現雙重目的性,即事件溯源使用事件來表示狀态更改,但使用事件實作Saga協同,需要聚合即使沒有狀态更改也必須發出事件;
    • 解決方法:使用編排式來實作複雜的Saga;

3.2 建立編排式Saga

Saga編排器由服務的方法建立,會執行建立和更新聚合兩項操作,該服務必須保證則兩個操作在同一個事物中完成;是以取決于使用的事件資料庫類型;

當關系型資料庫作為事件存儲庫時,應該如何建立Saga編排器:

  • 比較簡單,使用

    @Transactional

    注解,使Eventuate Local架構與Eventuate Tram Saga架構在同一個ACID事務中更新時間存儲庫并建立Saga編排器即可;

當非關系型資料庫作為事件存儲庫時,應該如何建立Saga編排器:

  • 由于NoSQL資料庫的事務模型功能有限,應用程式将無法以原子方式建立或更新兩個不同的對象;
  • 服務必須具有一個事件處理程式,該事件處理程式将建立Saga編排器來響應聚合發出的領域事件;

3.3 使用事件處理程式建立Saga編排器的案例

《微服務架構設計模式》讀書筆記 | 第6章 使用事件溯源開發業務邏輯 #yyds幹貨盤點# 前言1. 使用事件溯源開發業務邏輯概述2. 實作事件存儲庫3. 同時使用Saga和事件溯源4. 本章小結最後

好處:保證松耦合,因為OrderService之類的服務不再明确地執行個體化Saga;

問題:如何處理重複事件保證幂等性,解決方法如下:

  • 從事件的唯一屬性中導出Saga的ID;有多種選擇,其中一種是使用發出事件的聚合的ID作為Saga的ID,适用于為響應聚合建立事件而建立的Saga;
  • (有效)使用事件ID作為Saga ID;因為事件ID的唯一性能保證Saga ID也是唯一的;

3.4 實作基于事件溯源的Saga參與方

  • 指令式消息的幂等處理;
    • 很容易解決:Saga參與方在處理消息時生成的事件中記錄消息ID;在更新聚合之前,Saga參與方通過在事件中查找消息ID來驗證它之前是否處理過該消息;
  • 以原子方式發送回複消息;
    • 解決方法:讓Saga參與方繼續向Saga編排器的回複通道發送回複消息;
      • 當Saga指令處理程式建立或更新聚合時,它會安排将SagaReplyRequested僞事件與聚合發出的實際事件一起儲存在事件存儲庫中;
      • SagaReplyRequested僞事件的事件處理程式使用事件中包含的資料構造回複消息,然後将其寫入Saga編排器的回複通道;
      • 如3.5點圖所示;

3.5 基于事件溯源的Saga參與方的例子

下圖顯示了Accounting Service如何處理Saga發送的Authorize Command;Accounting Service使用Eventuate Saga架構,該架構用于編寫使用事件溯源的Saga;
《微服務架構設計模式》讀書筆記 | 第6章 使用事件溯源開發業務邏輯 #yyds幹貨盤點# 前言1. 使用事件溯源開發業務邏輯概述2. 實作事件存儲庫3. 同時使用Saga和事件溯源4. 本章小結最後

3.6 實作基于事件溯源的Saga編排器

  • 使用事件溯源持久化Saga編排器;
    • 可以使用以下事件持久化Saga:
      • SagaOrchestratorCreated:Saga編排器已建立;
      • SagaOrchestratorUpdated:Saga編排器已更新;
  • 可靠地發送指令式消息;
    • 關鍵在于如何以原子方式更新Saga的狀态并發送指令;
    • 對于NoSQL事件存儲庫,關鍵在于持久化SagaCommandEvent,它表示要發送指令;然後事件處理程式訂閱SagaCommandEvents并将每個指令式消息發送到适當的通道;
    • 詳情請見下圖:
《微服務架構設計模式》讀書筆記 | 第6章 使用事件溯源開發業務邏輯 #yyds幹貨盤點# 前言1. 使用事件溯源開發業務邏輯概述2. 實作事件存儲庫3. 同時使用Saga和事件溯源4. 本章小結最後
  • 確定隻處理一次回複消息;
    • 類似前面描述的機制;編排器将回複消息的ID存儲在處理回複時發出的事件中;

4. 本章小結

  • 事件溯源将聚合作為一系列事件持久化儲存。每個事件代表聚合的建立或狀态更改。應用程式通過重放事件來重建聚合的目前狀态。事件溯源保留領域對象的曆史記錄,提供準确的審計日志,并可靠地釋出領域事件;
  • 快照通過減少必須重放的事件數來提高性能;
  • 事件存儲在事件存儲庫中,該存儲庫是資料庫和消息代理的混合。當服務在事件存儲庫中儲存事件時,它會将事件傳遞給訂閱者;
  • Eventuate Local是一個基于MySQL和Apache Kafka的開源事件存儲庫。開發人員使用Eventuate Client架構來編寫聚合和事件處理程式;
  • 使用事件溯源的一個挑戰是處理事件的演變。應用程式在重放事件時可能必須處理多個事件版本。一個好的解決方案是使用向上轉換,當事件從事件存儲庫加載時,它會将事件更新到最新版本;
  • 在事件溯源應用程式中删除資料非常棘手。應用程式必須使用加密和假名等技術,以遵守歐盟GDPR等法規,確定在應用程式中徹底清除個人資料;
  • 事件溯源可以很簡單實作基于協調的Saga。服務具有事件處理程式,用于監聽基于事件溯源的聚合釋出的事件;
  • 我們也可以使用事件溯源技術實作Saga編排器。你可以編寫專門使用事件存儲庫的應用程式;

最後

::: hljs-center

新人制作,如有錯誤,歡迎指出,感激不盡!

:::

::: hljs-center

歡迎關注公衆号,會分享一些更日常的東西!

:::

::: hljs-center

如需轉載,請标注出處!

:::

繼續閱讀