成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

事件溯源(Event Sourcing)和命令查詢責任分離(CQRS)經驗

系統
CQRS 和事件源并非魔法。在開始你的旅程之前,理解這兩種模式的許多影響至關重要。

這篇文章是實現一個基于 CQRS 和事件溯源原則的應用程序,描述這個過程的方式,我相信分享我面臨的挑戰和問題可能對一些人有用。特別是如果你正在開始自己的旅程。

一、業務背景

項目的背景與空中交通管理(ATM)領域相關。我們為一個 ANSP(航空導航服務提供商)設計了一個解決方案,負責控制特定地理區域。這個應用程序的目標很簡單:計算并持久化飛行數據。流程大致如下。

在飛機穿越其領空之前的幾個小時,ANSP會收到來自 Eurocontrol 的信息,這個組織負責管理整個歐洲的航空交通。這些信息包含計劃數據,如飛機類型、起飛地點、目的地、請求的航路等。一旦飛機到達了 ANSP 的 AOR(責任區域,ANSP負責控制和監控航班的區域),我們就可以從各種來源接收輸入:航跡更新(飛行當前位置是什么)、修改當前航路的請求、由軌跡預測系統觸發的事件、來自沖突檢測系統的警報等。

雖然我們可能需要同時處理多個潛在的并發請求,但在吞吐量方面,與 PayPal 或 Netflix 沒法相提并論。

盡管如此,該應用程序是安全關鍵環境的一部分。在發生關鍵故障的情況下,我們不會損失金錢或客戶,但我們可能會失去人的生命。因此,實現一個可靠、響應迅速且具有彈性的系統,以保證數據一致性和完整性,顯然是首要任務。

二、CQRS、事件溯源

這兩種模式實際上都相當容易理解。

1.CQRS

CQRS(命令查詢責任分離)是一種將寫入(命令)和讀取(查詢)分離的方式。這意味著我們可以有一個數據庫來管理寫入部分。而讀取部分(也稱為視圖或投影)是從寫入部分派生出來的,可以由一個或多個數據庫來管理(取決于我們的用例)。大多數情況下,讀取部分是異步計算的,這意味著兩個部分并不嚴格一致。我們稍后會回到這一點。

CQRS 背后的想法之一是數據庫很難同時高效地處理讀寫操作。這可能取決于軟件供應商的選擇、應用的數據庫調優等。例如,Apache Cassandra 在持久化數據方面效率很高,而 Elasticsearch 對搜索非常出色。使用 CQRS 真的是利用解決方案的優勢的一種方式。

此外,我們還可以決定處理不同的數據模型。再次強調,這取決于需求。例如,在報告視圖上使用的模型,另一個在寫入部分持久化期間效率高的非規范化模型等。

關于視圖,我們可能決定實現一些與消費者無關的視圖(例如公開特定的業務對象),或者一些專門針對消費者的視圖。

2.事件溯源

根據 Martin Fowler 對事件溯源的定義:

確保應用狀態的所有更改都存儲為事件序列

這意味著我們不存儲對象的狀態。相反,我們存儲影響其狀態的所有事件。然后,要檢索對象狀態,我們必須讀取與該對象相關的不同事件,并逐個應用它們。

3.CQRS + 事件溯源

這兩種模式經常被組合在一起。在 CQRS 之上應用事件溯源意味著將每個事件持久化在我們應用程序的寫入部分。然后,讀取部分是從事件序列派生出來的。

在我看來,實現 CQRS 時并不需要事件溯源。然而,反之則不一定成立。

事實上,對于大多數用例,在實現事件溯源時需要 CQRS,因為我們可能希望以 O(1) 的時間復雜度檢索狀態,而不必計算 n 種不同的事件。一個例外是簡單的審計日志用例。在這里,我們不需要管理視圖(也不需要狀態),因為我們只對檢索日志序列感興趣。

三、領域驅動設計

領域驅動設計(DDD)是一種處理與領域模型相關的軟件復雜性的方法。它由 Eric Evans 在 2004 年的《Domain-Driven Design: Tackling Complexity in the Heart of Software》一書中引入。

我們不會介紹所有不同的概念,但如果你對此不熟悉,我強烈建議你去看一看。不過,我們將只介紹在 CQRS/事件溯源應用程序環境中有用的概念。

DDD 帶來的第一個概念是聚合(Aggregate)。聚合是一組領域對象,從數據變更角度來看,它們被視為一個單元。在聚合內部的事務必須保持原子性。

與此同時,聚合通過不變式來強制執行自己的數據一致性和完整性。不變式就是一個規則,無論如何變化,它都必須保持為真。例如,標準終端到達航線(STAR,基本上是著陸前的預定義航線)始終與給定機場相關聯。一個不變式必須強制執行這樣一個規則:目的機場不能在沒有更改 STAR 的情況下被修改,并且這個 STAR 與該機場是有效的。

此外,作為聚合的外觀對象(處理輸入并將業務邏輯委托給子對象的對象)被稱為聚合根。

關于組成聚合的對象,我們需要區分實體和值對象。實體是具有標識的對象,它不是由其屬性定義的。一個人的年齡會隨著時間的推移而改變,但他/她仍然是同一個人。另一方面,值對象僅由其屬性定義。不同城市的地址是不同的地址。前者是可變的,而后者是不可變的。此外,實體可以有自己的生命周期。例如,一個航班首先準備起飛,然后是空中飛行,最后著陸。

在模型定義中,實體應盡可能簡單,并專注于其標識和其生命周期。在 CQRS/事件溯源應用程序的上下文中,實體是一個關鍵元素,因為大多數情況下,在聚合內進行的更改是基于它們的生命周期。例如,至關重要的是確保每個實體實現了一個函數,用于確定它是否與另一個實體實例相等。這可以通過比較標識符或一組相關屬性來實現,從而保證了一個標識。

既然我們已經了解了實體的概念,讓我們回到不變式。為了定義它們,我們使用了受 BDD(行為驅動開發)格式啟發的語言:

Given [entity] at state [state]
When [event] occurs
We shall [rules]

領域驅動設計(DDD)是一種處理與領域模型相關的軟件復雜性的方法。它由 Eric Evans 在 2004 年的《Domain-Driven Design: Tackling Complexity in the Heart of Software》一書中引入。

我們不會介紹所有不同的概念,但如果你對此不熟悉,我強烈建議你去看一看。不過,我們將只介紹在 CQRS/事件溯源應用程序環境中有用的概念。

DDD 帶來的第一個概念是聚合(Aggregate)。聚合是一組領域對象,從數據變更角度來看,它們被視為一個單元。在聚合內部的事務必須保持原子性。

與此同時,聚合通過不變式來強制執行自己的數據一致性和完整性。不變式就是一個規則,無論如何變化,它都必須保持為真。例如,標準終端到達航線(STAR,基本上是著陸前的預定義航線)始終與給定機場相關聯。一個不變式必須強制執行這樣一個規則:目的機場不能在沒有更改 STAR 的情況下被修改,并且這個 STAR 與該機場是有效的。

此外,作為聚合的外觀對象(處理輸入并將業務邏輯委托給子對象的對象)被稱為聚合根。

關于組成聚合的對象,我們需要區分實體和值對象。實體是具有標識的對象,它不是由其屬性定義的。一個人的年齡會隨著時間的推移而改變,但他/她仍然是同一個人。另一方面,值對象僅由其屬性定義。不同城市的地址是不同的地址。前者是可變的,而后者是不可變的。此外,實體可以有自己的生命周期。例如,一個航班首先準備起飛,然后是空中飛行,最后著陸。

在模型定義中,實體應盡可能簡單,并專注于其標識和其生命周期。在 CQRS/事件溯源應用程序的上下文中,實體是一個關鍵元素,因為大多數情況下,在聚合內進行的更改是基于它們的生命周期。例如,至關重要的是確保每個實體實現了一個函數,用于確定它是否與另一個實體實例相等。這可以通過比較標識符或一組相關屬性來實現,從而保證了一個標識。

既然我們已經了解了實體的概念,讓我們回到不變式。為了定義它們,我們使用了受 BDD(行為驅動開發)格式啟發的語言:

應用程序設計

簡而言之,應用程序接收命令并發布內部事件。這些事件被持久化到事件存儲中,并發布給處理程序,這些處理程序負責更新視圖。我們還可以決定在視圖之上實現一個服務層(稱為讀處理程序)。

現在,讓我們詳細看看不同的場景。

2.聚合創建

命令處理程序接收一個 CreateFlight 命令,并在領域存儲庫中檢查實例是否存在。這個領域存儲庫管理聚合實例。它首先在緩存中進行檢查,如果對象不存在,則會在事件存儲中進行檢查。事件存儲是一個用于持久化事件序列的數據庫。我會稍后詳細說明我認為一個好的事件存儲是什么。在這種情況下,事件存儲仍然為空,因此存儲庫不會返回任何內容。

命令處理程序負責觸發不變式。在出現失敗的情況下,我們可以同步拋出異常來指示業務問題。否則,命令處理程序將發布一個或多個事件到事件總線。事件的數量取決于內部數據模型的粒度。在我們的場景中,我們假設發布了一個單一的 FlightCreated 事件。

在此事件上觸發的第一個組件是領域處理程序。這個組件負責根據實現的邏輯更新領域聚合。通常,邏輯被委托給聚合根(充當外觀,但也可以將底層邏輯委托給子域對象)。請記住,聚合必須始終保持一致,并且還必須通過驗證不變式來強制執行數據完整性。

如果處理程序成功(未引發業務錯誤),則事件將被持久化到事件存儲中,并且緩存將使用最新的聚合實例進行更新。

然后,觸發視圖處理程序來更新其對應的視圖。就像在普通的發布-訂閱模式中一樣,視圖可以只訂閱它感興趣的事件。也許在我們的情況下,視圖 2 是唯一對 FlightCreated 事件感興趣的視圖。

3.聚合更新

第二種情景是更新現有的聚合。在接收到 UpdateFlight 命令時,命令處理程序會請求存儲庫返回最新的聚合實例(如果有的話)。

如果實例已經在緩存中,則無需與事件存儲交互。否則,存儲庫將觸發所謂的重新裝載過程。

這個過程是根據存儲的事件序列計算聚合實例的當前狀態的一種方式。從事件存儲中檢索的每個事件(比如 FlightCreated、DepartureUpdated 和 ArrivalUpdated)都會被發布到事件總線。第一個領域處理程序觸發 FlightCreated 時會實例化一個新的聚合(根據事件本身提供的信息,在內存中創建一個新的對象實例)。然后其他領域處理程序(由 DepartureUpdated 和 ArrivalUpdated 事件觸發)將更新剛剛創建的聚合實例。最終,我們能夠根據存儲的事件計算出狀態。

一旦計算出狀態,對象實例就會被放入緩存并返回給命令處理程序。然后,其余的流程與聚合創建情景相同。

關于重新裝載過程還有一件事需要補充。如果一個聚合不在緩存中,而我們為一個特定的聚合實例存儲了 1000 個事件,那么會花費很長時間來計算其狀態。有一個已知的緩解措施叫做快照。

我們可以決定在每 n 個事件中持久化聚合的當前狀態作為一個快照。這個快照也會包含在事件存儲中的位置。然后,重新裝載過程將簡單地從最新的快照開始,并從指定的位置繼續。快照還可以根據其他策略類型創建(如果重新裝載時間超過某個閾值等)。

4.如何處理事件?

我想再回顧一下我們對命令和事件的區分。首先,有必要區分內部事件和外部事件。外部事件是由另一個應用程序產生的,而內部事件是由我們的應用程序生成的(基于外部命令)。

我們就如何在我們的應用程序中技術性地處理外部事件進行了一場有趣的辯論。我的意思是,真正的事件指的是已經在過去發生的事情(比如雷達軌跡)。

實際上有兩種可能的處理方法:

  • 第一種方法是將事件視為命令。這意味著我們必須首先通過一個命令處理程序,驗證不變式,然后生成一個內部事件。
  • 第二種方法是繞過命令處理程序,直接將事件持久化到事件存儲中。畢竟,如果我們談論的是一個真實事件,那么驗證不變式等操作實際上是沒有什么用的。然而,檢查事件的語法仍然很重要,以確保我們不會污染事件存儲。

如果我們選擇第二個選項,可能會有興趣在聚合重新裝載期間實現規則。

讓我們舉一個雷達軌跡發布飛行位置的例子。如果生產者無法保證消息的順序,我們還可以持久化一個時間戳(由生產者生成),并以這種方式計算狀態:

if event.date > latestEventDate {  // Compute the state
  latestEventDate = event.date} else {  // Discard the event}

這個規則將確保狀態僅基于最新生成的事件。這意味著持久化一個事件不一定會影響當前狀態。

在第一種方法中,在持久化事件之前會實現這樣的規則。

5.事件模型

在事件存儲中持久化的事件是否需要創建一個統一的模型?在我看來,答案是否定的(至少大部分情況下是)。

首先,因為我們可能希望隨著時間推移持久化不同的模型版本。在這種情況下,我們必須實現一種策略,將一個模型版本的事件映射到另一個模型版本。

我想用一個具體的例子來說明另一個好處。假設一個應用程序接收來自系統 A 和系統 B 的事件。這兩個系統基于各自的數據模型發布飛行事件。如果我們創建一個通用數據模型 C,我們需要在持久化事件之前將 A 轉換為 C 和 B 轉換為 C。然而,在項目的某個階段,我們只對來自 A 和 B 的某些信息感興趣。這意味著 C 只是 A 和 B 的一個子集。

但是如果以后我們需要對應用程序進行一些改進,并管理來自 A 和 B 的額外元素怎么辦?因為事件是使用 C 格式持久化的,所以這些元素就會被簡單地丟失。另一方面,如果我們決定持久化 A 和 B 格式,我們可以簡單地對命令處理程序進行一些改進,以管理這些元素。

四、最終一致性

1.理論

最終一致性是由 CQRS(大多數情況下)引入的一個概念。理解其影響和后果非常重要。

首先,值得一提的是有不同的一致性級別。

最終一致性是一個模型,我們可以確保數據會被復制(從 CQRS 應用程序的寫入部分到讀取部分)。問題在于我們無法確切保證何時復制完成。這會受到各種因素的影響,比如整體吞吐量、網絡延遲等。這是最弱的一致性形式,但提供了最低的延遲。

在 CQRS 應用程序中應用最終一致性意味著在某個時刻,寫入部分可能與讀取部分不同步。

相反地,我們可以找到強一致性模型。除非我們在分布式系統中使用相同的數據庫來管理讀取和寫入,或者我們通過使用兩階段提交向惡魔出賣了我們的靈魂,否則在分布式系統中我們不應該達到這種一致性級別。

最接近的實現方法是,如果我們有兩個不同的數據庫,那就在單個線程中管理所有操作。這個線程將負責將數據持久化到寫入數據庫和讀取數據庫(們)。一個線程還可以專門用于單個聚合實例,并按順序處理傳入的命令。然而,如果在同步視圖時發生瞬態錯誤,會有什么影響?我們需要補償其他視圖和 CQRS 應用程序的寫入部分嗎?我們需要實現錯誤重試循環嗎?我們需要通過暫停命令處理程序來停止新的傳入事件,應用斷路器模式嗎?解決顯然會發生的瞬態錯誤是很重要的(凡是可能出錯的地方遲早會出錯)。

在最終一致性和強一致性兩種一致性模型之間,我們可以找到許多不同的模型:因果一致性、順序一致性等。舉例來說,客戶端單調一致性模型僅在會話(應用程序或服務實例)內保證強一致性。因此,實現 CQRS 應用程序并不只是在最終一致性和強一致性之間做出選擇。

我個人的觀點是:由于我們幾乎無法保證強一致性,讓我們盡可能地接受最終一致性。然而,前提是要精確理解其對系統其余部分的影響。

2.例子

讓我們看一個我在項目中遇到的具體例子。

其中一個挑戰是管理每架飛機的唯一標識符。我們不得不處理來自外部系統(公司外部)的事件,這些系統中的標識符并不相同。對于一個通道,標識符是一個復合標識符(出發機場 + 出發時間 + 飛機標識符 + 到達機場),而另一個通道則發送每架飛機的唯一標識符(但第一個通道不知道)。我們的目標是管理我們自己的唯一標識符(稱為 GUFI,即全局唯一飛行標識符),并確保每個事件都對應于正確的 GUFI。

最簡單的解決方案是確保每個傳入的事件都在我們應用程序的特定視圖中進行查找,以關聯相應的 GUFI。但如果這個視圖是最終一致的呢?在最壞的情況下,我們可能會有與同一飛行相關的事件,但使用不同的 GUFIs 進行存儲(相信我,這是一個問題)。

一個解決方案可能是將這個 GUFI 的管理委托給另一個強一致性的服務。

在一次問答環節中,Greg Young 提供了另一個解決方案。我們可以實現一種緩沖區,其中只包含我們應用程序處理的 n 個最新事件。如果視圖中不包含我們正在尋找的數據,我們必須在這個緩沖區中檢查,以確保它不是剛剛在視圖之前接收到的。n 越大,減輕寫入和讀取之間的這種不一致性窗口的機會就越大。

這個緩沖區可以使用像 Hazelcast、Redis 等解決方案進行分布式處理,也可以局部于應用程序實例。在后一種情況下,我們可能需要實現一個分片機制,使用哈希函數將相關對象的事件始終分發到相同的應用程序實例(最好是使用一種一致性哈希函數,以便輕松擴展)。

五、并發管理

幾個月前我已經創建了一篇文章,描述了使用事件源管理并發更新的好處。

簡而言之,擁有事件存儲可能會幫助我們找到比悲觀或樂觀方法更聰明的解決方案來處理并發更新。

此外,在數據模型中應用正確的粒度也是項目成功的關鍵。

六、選擇事件存儲

我們可以決定使用任何類型的數據庫來持久化事件序列。然而,最優解往往是為事件源構建的解決方案。

例如,隔離一個聚合實例是必須考慮的事情。假設所有事件都存儲在一個單一表中。這個表會隨著時間不斷增長,在聚合重建時,我們將不得不過濾與一個特定聚合實例相關的事件。重建一個聚合的時間將取決于持久化的事件總數,即使其中一些事件與我們感興趣的實例無關。一個好的解決方案可能是為每個聚合實例擁有一個表/存儲桶,以隔離事件。我們稱這個概念為流(stream)。一個流總是與一個聚合實例相關聯(在大多數用例中)。

以下是我們考慮選擇事件存儲時的要求:

(1) 寫入:

  • 恒定的寫入延遲:無論流的大小如何,持久化事件的延遲都必須保持恒定
  • 原子性:可以在單個事務中追加多個事件
  • TTL 管理:根據創建日期自動丟棄事件
  • 無模式:可以存儲多種事件類型和版本

(2) 讀取:

  • 按寫入順序讀取事件
  • 從特定序列號讀取(因為快照)
  • 在給定流中保持恒定的讀取性能,不受其他流的影響
  • 圖形用戶界面(GUI)
  • 緩存管理

(3) 并發:

  • 樂觀并發模型
  • 冪等性管理

(4) 產品監控

(5) 解決方案支持

(6) 安全性:

  • 加密(傳輸)
  • 身份驗證
  • 授權管理

(7) 擴展性

(8) 備份

每個上下文都是獨特的,我相信你會有自己的要求,但這至少可能是一個起點。

七、結論

CQRS 和事件源并非魔法。在開始你的旅程之前,理解這兩種模式的許多影響至關重要。否則,在技術和功能層面都很容易造成徹底的混亂。

然而,一旦你對約束和缺點有了明確的理解,CQRS 和/或事件源可能是許多問題的很好解決方案。

責任編輯:趙寧寧 來源: 技術的游戲
相關推薦

2025-03-10 00:15:00

Axon開源框架

2022-09-07 08:58:58

Node.js框架

2024-09-20 08:04:54

2020-05-11 15:23:58

CQRS代碼命令

2024-02-01 12:38:22

事件流事件溯源系統

2022-03-07 06:34:22

CQRS數據庫數據模型

2023-10-15 16:39:29

2023-12-13 10:44:57

事件驅動事件溯源架構

2011-07-04 14:50:49

QT Event 事件

2025-04-27 10:14:57

2023-02-19 12:44:07

領域事件DDD

2023-02-26 10:59:51

2010-06-23 11:24:23

Linux Bash命

2020-12-04 07:51:24

CQRS模型查詢

2021-06-02 11:28:53

查詢權責分離

2009-12-25 17:17:45

shell命令

2025-04-27 01:11:11

GolangKafkaSaga

2011-08-29 14:59:26

QtEvent事件

2017-09-01 10:48:33

分離CQRS性能

2022-08-22 10:29:16

APT溯源反溯源APT攻擊
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 精品国产一区二区三区久久久蜜月 | 日韩成人在线观看 | cao在线 | 女人精96xxx免费网站p | 99精品视频免费观看 | 在线日韩精品视频 | 九九免费在线视频 | 日韩精品在线观看一区二区三区 | 日本一区二区三区四区 | www.日韩| 韩日精品一区 | 国产精品美女久久久久aⅴ国产馆 | 欧美精品一区二区三区四区 | 一区二区三区在线免费观看 | 91资源在线观看 | 国产精品久久国产精品99 gif | 久久国产精品-久久精品 | 久久精品色欧美aⅴ一区二区 | 欧美色综合一区二区三区 | 久久成人精品视频 | 成年人免费在线视频 | 国产精品视频久久 | 国产xxxx在线| 3p视频在线观看 | 中文字幕精品一区 | 久久中文字幕一区 | caoporn国产| 国产区一区二区三区 | 精品无码久久久久久国产 | 妞干网视频| 日韩一区二区三区视频在线观看 | 亚洲综合精品 | 天堂成人国产精品一区 | 久久精品男人的天堂 | 亚洲一区二区三区在线播放 | 中文字幕乱码一区二区三区 | 日韩在线视频一区二区三区 | 亚洲成人久久久 | 午夜免费视频观看 | 日韩欧美综合在线视频 | 成人欧美一区二区三区黑人孕妇 |