跳轉到

深入淺出 Event Sourcing 和 CQRS

Event Sourcing也叫事件溯源,是這些年另一個越來越流行的概念,是大神 Martin Fowler 提出的一種架構模式。簡單來說,它有幾個特點:

  • 整個系統以事件為驅動,所有業務都由事件驅動來完成。
  • 事件是一等公民,系統的數據以事件為基礎,事件要保存在某種存儲上。
  • 業務數據只是一些由事件產生的視圖,不一定要保存到數據庫中。

這麼說可能還是比較難以理解,我們來舉個例子。這是一個賬戶餘額管理的例子:

img

在這個圖中,中間的是我們的賬戶對象,它有幾個時間處理函數create(), deposit(), withdraw(),分別用於處理新建賬戶、賬戶存款和取款的操作。

左邊的就是一個個的事件,它是一個事件的流,根據用戶請求或者從其他地方產生。在這裡例子當中,有 3 個事件:AccountCreated, AccountDeposited, AccountWithdrawed,分別相當於賬戶創建的事件,存款的事件和取款的事件。當這些事件產生的時候,我們會觸發上面的 Account 對象的相應的處理函數。

右邊的就是這個 Account 對象處理完左邊的 4 個事件以後,最新的數據狀態。具體的處理過程就是:

  1. 系統產生一個新建賬戶的事件AccountCreatedAccount對象來處理這個事件,事件裡面的 Id 是 1234,系統先嘗試著找 id 是 1234 的賬戶,發現沒有,於是新建一個Account對象,並在它上面調用create()的處理函數,也就是初始化了 id 和余額。
  2. 然後,有一個AccountDeposited的事件,對應的Account對象的 id 是 1234,系統找到之前創建的對象,在它上面應用deposit()處理函數,也就是增加的餘額的操作,更新了賬戶餘額。
  3. 系統又收到一個存款事件,跟上面一樣,又更新了一次餘額。
  4. 收到一個取款事件,還是找到 id 是 1234 的賬戶,在它上面調用withdraw()的處理函數,進行取款操作,更新余額。
  5. 最後,1234 這個賬戶的餘額是 200,也就是右邊的數據狀態。

同時,上面的這些事件需要持久保存在數據庫或其他地方,而 account 的數據狀態卻不需要保存,我們只是在需要獲得 account 當前的數據狀態的時候,通過這個 account 相關的事件,調用他們的處理函數,重新生成當前狀態。當然,每次都這樣調用處理函數勢必會造成資源的浪費,因為它需要從數據庫中取得所有這個 account 的事件,然後依次調用處理函數。所以一般我們可以把這個 account 的最新狀態,以一種視圖的方式保存在數據庫中。

上面這個方式和過程,就是我們說的 Event Sourcing,也就是以事件為源的處理模式。

Event Sourcing 的構成

通過上面的例子,我們理解了 Event Sourcing(事件溯源),下面我們再來看看 Event Sourcing 包含哪些部分。

聚合對象

在上面的例子中,Account對象就是一個聚合對象,它裡麵包含賬戶的基本信息,也包含了對賬戶操作時的處理方法,也就是幾個事件處理函數。了解領域驅動設計的人這時候應該就想到,這個 Account 對像其實就是一個領域模型,Account 這個領域模型需要的業務操作,由它自己提供。

每個聚合對像都有一個 Id,用於唯一標識這個對象,所以系統中不同的賬戶就會有不同的 Account 對象。

Event Store

我們說了,在 Event Sourcing 模式當中,所有的事件都是要保存到數據庫(或其他存儲,下面就直接說數據庫了)中的,這個存儲就叫 Event Store。

每個事件應該也包含一個它要處理的聚合對象的 id,以及事件的順序,查詢的時候就是根據聚合對象的 id 從數據庫中找到相關的事件,並按照生成的事件或序號排序。

Event Store 除了提供事件數據的存儲、查詢功能以外,還可以提供事件的重現等功能。事件的重現,就是將截止某一個時間的所有事件取出來,調用他的處理函數,生成當時那個時間點的業務狀態。所以在重現之前,如果我們的業務數據的狀態,通過視圖的形式保存到了數據庫中,我們需要先清除相應的數據。正是由於 Event Sourcing 模式的這個以事件為源的特性,所以我們才有可能提供這樣的歷史重現的功能。

聚合資源庫

一般情況下,我們的聚合對象的數據狀態是不會保存在數據庫當中的。每當系統要獲得某一個賬戶的數據的時候,都是從 Event Store 當中取出所有相關聚合對象的事件,然後依次的調用這些事件的處理方法,“聚合”出該領域對象最新的數據狀態。這個,就是聚合資源庫需要提供的功能。

視圖

上面我們也說了,如果每次都重新“聚合”出對象,獲取當前的狀態,會浪費很多資源。所以,我們可以在某個事件發生的時候,將這個聚合對象的最新數據狀態,寫到一個表中,這個表可以叫做物化視圖。

查詢

由於我們提供了專門的視圖表,將聚合對象的最新狀態保存在數據庫中,那我們在查詢的時候,可以通過該物化視圖去查詢,而不是通過聚合對象的資源庫去查詢。

Event Sourcing 與 CQRS

CQRS,是 Command Query Responsibility Segregation 的縮寫,也就是通常所說的讀寫隔離。在上面,我們說,為了性能考慮,將聚合對象的數據狀態用物化視圖的形式保存,可以用於數據的查詢操作,也就是我們把數據的更新與查詢的流程隔離開來。我們通過事件來更新聚合對象的數據狀態,同時由另一個處理器處理相同的事件,來更新物化視圖的數據。

所以,Event Sourcing 與 CQRS 有著天然的聯繫,所以也經常會有人把他們放在一起討論。實際上,CQRS 是在使用 Event Sourcing 模式以後,又使用了物化視圖的情況下,所產生的額外的好處。

下圖就是使用 Event Sourcing 好 CQRS 模式以後的一個簡單的流程圖:

img

  1. 對於 Command 類型的請求(需要修改數據),web 層會走通過 Event Sourcing 更新聚合對象的流程,這時會有一個 Event Handler 的處理類監聽相應事件,更新物化視圖。
  2. 對於 Query 類型的請求,web 層會通過相應的 DAO 獲取數據返回。

Event Sourcing 的優點與缺點

Event Sourcing 之所以會越來越受到關注,是因為它的一些優點:

  1. 方便進行溯源與歷史重現
    這個“溯源”的意思是,我們可以通過對保存的事件的分析,知道現在的系統的狀態,是怎麼一步一步的變成這樣的。這在一個大型的業務複雜的應用系統裡尤為有用。如果沒有使用 Event Sourcing 模式,即使我們使用完備的 log 機制,提供 log 查詢分析,也很難追溯數據的變化過程。
    此外,我們還可以根據歷史的事件,重新生成所有的業務狀態數據。我們甚至可以指定我要生成到具體某一時刻的狀態。這就好像我們可以自由的穿梭在我們的系統運行過程當中,查看任何一個時間點的狀態。
  2. 方便 Bug 的修復
    由於我們可以重現歷史,所以當發現一個 bug 以後,我們可以在修復完以後,直接重新聚合我們的業務數據,修復我們的數據。如果使用傳統的設計方法,我們就需要通過 SQL 或者寫一段程序,去手動的修改數據庫,以使它達到正常的狀態。如果這個 bug 存在的時間較長,牽扯的數據較多,這將會是一個非常麻煩的事情。
    同時,由於我們可以通過事件分析業務的過程,這也經常能幫助我們發現問題。
  3. 能提供非常好的性能
    在 Event Sourcing 模式下,事件數據的保存是一個一直新增的寫表操作,沒有更新。這在很多情況下都能夠提供一個非常好的寫的性能,讓系統的接收事件的吞吐量可以很高。
    然後聚合對像根據事件的聚合 Id,獲取所有相關的事件,“聚合”出對象,調用業務方法。但是它的結果又不需要寫數據庫,這裡面只有一個讀操作,其他的操作都是在內存中。
    最後,由另一個 Event Handler 處理這些事件,更新物化視圖的表。在更新數據的時候,我們只需要鎖記錄,所以這個更新的過程也可以很快。
    通過這種模式,我們的系統的壓力最終基本上都落在數據庫上,整個系統裡不會有太多鎖和等待(只有並發的在同一個聚合對像上處理,才會等待,例如用戶同時發了多個請求進行存款取款操作),就可以提供非常好的吞吐量。
  4. 方便使用數據分析等系統
    這個就很明顯了,用了 Event Sourcing 模式,我們的數據就是事件,我們只需要在現有的事件加上需要的處理方法,來做數據分析;或者將 event 直接發送到某個分析系統。我們就不需要為了做數據分析,再在系統裡定義各種事件,發送事件等。

當然,它也有一些缺點:

  1. 開發思維的轉變
    最大的難點,估計就是對開發人員的思維方式的轉變。使用 Event Sourcing 模式,需要我們從設計角度,使用一定的領域驅動設計的方法,從開發角度,我們又需要使用基於事件的響應式編程思維。對於習慣了傳統的面向對象的程序員來說,這都是一個不小的挑戰。
  2. 沒有成熟完善的框架
    我們開發 Java 的應用,現在絕大多數情況下都會使用 Spring,也有很大一部分使用 Spring MVC(或 Spring Boot)。不管怎麼樣,都是基於 Spring 這個框架家族進行開發。但是,對於 Event Sourcing 模式來說,還沒有一個大而一統的框架,既能提高很好的 Event Sourcing 模式的實現,又能被廣泛接受,最好還能有一些廠商提高商業服務,保證整個生態的良性發展。
  3. 事件的結構的改變
    使用 Event Sourcing 模式,還有一個問題就是事件結構的改變。由於業務的變化,我們設計的事件,在結構上可能有一些改變,可能需要添加一些數據,或者刪除一些數據。那麼這時候,想要進行方才說的“歷史重現”就會有問題。這時我們就需要通過某種方式給他提供兼容。
  4. 從領域模型角度設計系統,而不是以數據庫表為基礎設計
    這其實不算是一個缺點,但是由於領域驅動設計並不是廣泛使用的軟件設計方式,很多開發人員對此不了解,相應的設計和開發方式也不熟悉,所以這也成為使用 Event Sourcing 模式開發需要解決的問題。
    領域驅動設計從業務分析出發,從領域模型設計著手設計一個系統,而在設計一個基於 Event Sourcing 模式的系統時,我們往往也要用到領域模型設計的一些方法,從領域模型設計開始,設計聚合對象和它的業務(事件),以及處理方法(Event Handler)。通過領域驅動設計,對複雜的應用系統往往能提供更好的設計。但是,這種設計方式又和我們常用的設計方法不一致,有一定的學習和實踐成本。

基於 Event Sourcing 的分佈式系統

如果要開發一個基於 Event Sourcing 模式的分佈式系統,最簡單的方式就是用 2 個服務分別提供讀和寫的功能。寫服務接收 Command 請求,觸發聚合對像上的處理函數,更新聚合數據。然後把這個事件發送到一個 MQ 隊列上。讀服務監聽這個隊列,獲取事件,更新相應的物化視圖的數據。同時所有的 Query 請求都由讀服務處理並返回。

對於寫服務,它的數據只有事件,是一個流式的寫操作,還有基於索引的查詢。對於讀服務,我們又可以部署多個應用,進一步提供數據查詢的性能。可以看到,通過這麼一個簡單的讀寫分離,我們就能大大提高系統的性能。

什麼時候使用 Event Sourcing

使用 Event Sourcing 有它的優點也有缺點,那麼什麼時候該使用 Event Sourcing 模式呢?

  1. 首先是系統類型,如果你的系統有大量的 CRUD,也就是增刪改查類型的業務,那麼就不適合使用 Event Sourcing 模式。Event Sourcing 模式比較適用於有復雜業務的應用系統。
  2. 如果你或你的團隊裡面有 DDD(領域驅動設計)相關的人員,那麼你應該優先考慮使用 Event Sourcing。
  3. 如果對你的系統來說,業務數據產生的過程比結果更重要,或者說更有意義,那就應該使用 Event Sourcing。你可以使用 Event Sourcing 的事件數據來分析數據產生的過程,解決 bug,也可以用來分析用戶的行為。
  4. 如果你需要係統提供業務狀態的歷史版本,例如一個內容管理系統,如果我想針對內容實現版本管理,版本回退等操作,那就應該使用 Event Sourcing。

Reference