Event Sourcing和CQRS

深入淺出Event Sourcing和CQRS

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

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

什麼是Event Sourcing

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

在這裏插入圖片描述

在這個圖中,中間的是我們的賬戶對象,它有幾個時間處理函數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模式以後的一個簡單的流程圖:

在這裏插入圖片描述

  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。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章