微服務架構設計模式 | 第6章 使用事件溯源開發業務邏輯

前言

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

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

()為補充,一般是書本里的內容;
[]符號為筆者筆注;


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

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

1.1 傳統持久化技術的問題

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

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

事件溯源采用基于領域事件的概念來實現聚合的持久化;它將每個聚合持久化為數據庫中的一系列事件,稱為事件存儲。

事件溯源將每個聚合作為一系列事件來持久化保存

圖解

  • 事件溯源不是將每個Order作為一行存儲在ORDER表中,而是將每個Order聚合持久化為EVENTS表中的一行或多行;
  • 應用程序創建或更新聚合時,它會將聚合發出的事件插入到EVENTS表中;
  • 應用程序通過從事件存儲中檢索并重放事件來加載聚合(如Eventuate Client框架),加載聚合的步驟:
    • 加載聚合的事件;
    • 使用其默認構造函數創建聚合實例;
    • 調用apply()方法遍歷事件;
  • 事件溯源通過加載事件和重放事件來重建聚合的內存狀態;

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

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

1.4 事件代表狀態的改變

  • 在事件溯源情況下,聚合主要決定事件及其結構;
  • 包括創建在內的每一個聚合狀態變化,都由領域事件表示;
  • 每當聚合的狀態發生改變時,它必須發出一個事件;
  • 事件中必須包含聚合執行狀態變化所需的數據;
  • 聚合的狀態由構成聚合對象的字段值組成;
事件中必須包含聚合執行狀態變化所需的數據

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

  • 基于事件溯源的應用程序中的命令方法通過生成事件來處理對聚合更新的請求;
  • 調用聚合命令方法的結果是一系列事件,表示必須進行的狀態更改;
調用聚合命令方法的結果是一系列事件
  • 生成事件并應用(apply)事件的做法將導致對業務邏輯的重構;事件溯源將命令方法重構為兩個或更多個方法;
    • 第一個方法process()接收命令對象參數,該參數表示具體的請求,并確定需要自行哪些狀態更改;它驗證命令對象的參數,并且在不更改聚合狀態的情況下,返回表示狀態更改的事件列表;如果無法執行該命令,則此方法通常會引發異常;
    • 其他方法apply()都將特定事件類型作為參數來更新聚合;這些方法與聚合產生的事件類型一一對應;重要的是要注意執行這些方法不會出現失敗,因為這些事件代表了一個已經發生的狀態變化;每個方法都會根據事件更新聚合;
    • 一個例子如下:
事件溯源框架例子

圖解

  • 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聚合

基于事件溯源的Order聚合
  • 業務邏輯通過命令來實現,這些命令發出事件并應用那些更新其狀態的事件;
  • 創建或更新基于JPA的聚合的每個方法,如createOrder()reviseOrder(),在事件溯源版本中都由process()apply()方法替代;
事件溯源使用process和apply替代原方法
  • 基于JPA聚合的修改訂單業務邏輯由三個方法組成:reviseOrder()confirmRevision()rejectRevision()
  • 事件溯源版本使用三個process()方法和一些apply()方法替代這三個的方法;

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

指兩個或多個請求同時更新同一聚合的情況;

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

1.9 事件溯源和發布事件

  • 使用輪詢發布事件;
    • 如下圖所示:


      使用輪詢發布事件
  • 使用事務日志拖尾技術來可靠地發布事件;
    • 本篇第2點詳解;

1.10 使用快照提升性能

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

使用快照避免加載聚合所有事件

1.11 冪等方式的消息處理

使用相同的消息多次安全地調用消息接收方,則消息接收方是冪等的;具體實現方式取決于事件儲存庫是關系型數據庫還是NoSQL數據庫;

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

1.12 領域事件的演化

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

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

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

每個級別可能發生的不同類型的更改
  • 服務的領域模型隨著時間的推移而發展,這些變化會自然發生;
  • 不向后兼容段更改都需要更改該事件類型的消費者;

通過向上轉換(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事件存儲庫編寫基于事件溯源的應用程序;它為開發基于事件溯源的聚合、服務和事件處理程序提供了框架基礎;

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

圖解

  • 通過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編排器的案例

使用事件處理程序創建Saga編排器

好處:保證松耦合,因為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;

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

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

  • 使用事件溯源持久化Saga編排器
    • 可以使用以下事件持久化Saga:
      • SagaOrchestratorCreated:Saga編排器已創建;
      • SagaOrchestratorUpdated:Saga編排器已更新;
  • 可靠地發送命令式消息
    • 關鍵在于如何以原子方式更新Saga的狀態并發送命令;
    • 對于NoSQL事件存儲庫,關鍵在于持久化SagaCommandEvent,它表示要發送命令;然后事件處理程序訂閱SagaCommandEvents并將每個命令式消息發送到適當的通道;
    • 詳情請見下圖:
可靠地發送命令式消息
  • 確保只處理一次回復消息
    • 類似前面描述的機制;編排器將回復消息的ID存儲在處理回復時發出的事件中;


4. 本章小結

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



最后

\color{blue}{\rm\small{新人制作,如有錯誤,歡迎指出,感激不盡!}}

\color{blue}{\rm\small{歡迎關注我,并與我交流!}}

\color{blue}{\rm\small{如需轉載,請標注出處!}}

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,443評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,530評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,407評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,981評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,759評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,204評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,263評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,415評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,955評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,650評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,892評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,675評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內容