1. 程式人生 > >當我們在說事件驅動的時候,我們在說什麼

當我們在說事件驅動的時候,我們在說什麼

  Martin Fowler是面向物件分析設計、重構等領域的頂級專家,也是敏捷開發的創始人之一,也是企業應用架構方面的頂級專家。

  這篇文章的初衷,是在之前的ThoughtWorks開發者大會中,他們發現,一般人們在說到事件時,發現不同的人往往說的不是同一件事情。所以就有了這篇文章,將幾種主要的事件模式整理出來,供大家參考。這樣,以後大家再討論事件啟動架構的時候,可以先弄清楚對方討論的是什麼模式。

  事件通知

  這一模式就是一個系統傳送一些事件訊息到另一些系統,以通知他們說我這個系統裡面的領域物件發生了改變。這個通知的一個關鍵點是,我的源系統並不關心對方系統收到這些通知以後的結果,甚至說,根據不期望有返回結果。如果說,在有些業務場景下,需要知道對方處理的結果的話,那也應該是通過另一個事件通知的邏輯來通知源系統。

  所以,事件通知模式非常好的實現了系統之間的隔離性,而且很容易實現,我們只需要一個訊息佇列就能實現,隨便一個開源的MQ伺服器就能滿足。

  但是,這也有一個潛在的問題,就是事件通知的這個流(也就是一個事件從A系統傳送到B系統,B系統處理完以後,又傳送另一個事件到C系統),也是很難管理或監控的,因為它散落在各個系統的程式碼裡。所以,當我們的系統變得複雜,系統(或者說服務)越來越多的時候,這些系統之間的事件及其流向,就很難維護了。但是,即便是這些弊端,事件通知模式還是很有用的,因為它非常簡單。

  使用事件通知還有一個需要注意的問題是,對事件的定義。事件是指的原系統發生了某件事情,導致領域物件的改變。但是很多人在使用的過程中,往往將事件和動作混淆。動作是發起請求的人希望目標系統執行的動作,而通知是源系統領域物件發生的事情。舉個例子來說,A是訂單服務,B是庫存服務,當有一個訂單時,要通知庫存服務去更新庫存。那麼通知和命令是這樣的:

  通知:A通知B有了一個新訂單,B自己根據業務決定如何根據訂單處理庫存)

  命令:A命令B修改庫存,這時候,A與B之間的關係就變了。

  這在理解上很好理解,但是在實際開發中很容易忽略,需要引起注意。

  有關事件通知,還有一點就是,事件不需要攜帶很多資料,而只是攜帶像id和對源資料的查詢連結之類的資料。還是用上面的訂單、庫存為例,訂單系統發出的事件中只有一個訂單id和用來查詢訂單id的url,庫存服務收到該事件以後,用這個url,加上訂單id,獲取訂單的資訊,這個資訊是庫存服務需要知道最少資料,如訂單商品和數量。

  個人認為,這種方式,雖然減少了訊息佇列中的資料傳輸,也減少了系統之間的資料結構的耦合性(目標系統需要知道原系統發出來的事件中的資料結構),但是,目標系統還是需要通過一定方式知道事件訊息的資料是什麼樣的,而且每次還要重新取資料,可能原系統為了它這個請求還要多寫一個介面,有點得不償失。

  攜帶狀態的事件傳遞

  這種模式跟上面的比,就是把目標系統處理事件時需要用到的資料都放在事件訊息裡。這種方式解決了上面說的一些問題。可能會帶來的問題中,事件的儲存量的增加應該算不上問題了。但是一個最大的問題就是,源系統和目標系統都需要知道事件中資料的結構。還有當事件中的資料結構發生改變,如增減欄位等,那需要目標系統也做相應修改。這一點本來就是不可避免的,即使是上面的不攜帶資料的方式,目標系統也需要更新最新的資料結構獲取資訊。

  事件溯源

  事件溯源模式的核心思想是,在一個系統中,任何的狀態的變化,都需要產生一個事件,並由這個事件觸發相應業務狀態的更改。在這種模式下,當前的業務狀態,是由這些事件以及它們的處理方法生成的。如果我們將這些事件儲存下來,在需要的時候,只要再重新觸發這些事件,讓這些事件的處理過程重新執行,就能夠重新生成業務狀態,甚至可以通過指定一個事件,來重新生成某一個時間點的資料狀態。這也是一些人常說的歷史重現。

  有關事件溯源,有一個錯誤的認識是,事件溯源不一定非要是非同步的。作者舉了一個非常好的例子,是git資源庫。一個git資源庫可以看作是一個事件溯源的應用,我們提交的一個個commit就是一個個的事件,所有的commit都是依次、同步的作用在這個版本控制系統中,我們的資源庫中的最新的檔案,就是這些commit事件依次作用產生的結果。

  對事件溯源的另一個誤區是,在事件溯源系統中的每個請求,在處理這個事件的時候,不需要知道整個系統的所有事件,而只需要知道它自己感興趣的那一類事件。還是以git資源庫為例。git是一個分散式的版本管理系統,如果我編輯了資源庫中的一個檔案,修改完以後commit;這時另一個人在他的電腦也修改了這個資源庫中的另一個檔案,也commit到他的本地資源庫。當我們兩個人把這兩個commit同步(push)到某個伺服器上的資源庫的時候,並不會衝突,git會根據我們的提交的時間,生成相應的commit。這時候伺服器上的資源庫裡面,提交日誌(相當於event log)裡面看到的就是合併後的commit。

  對於這個用git資源庫來類比事件溯源,可以這麼理解,每個人都可以把git資源庫clone到本地,相當於將這個基於事件溯源的應用系統進行分散式部署。假設我們每個人在修改檔案的時候,只允許修改一個檔案,提交後才能修改另一個檔案。每個提交的commit事件中都帶有這個檔案的id。這一個個的檔案相當於事件溯源系統中的領域物件,如一個訂單資訊,一個商品資訊,每個訂單每個商品都有它的全域性唯一ID。我們的每個事件就是在某一個領域物件上的更新操作,例如一個訂單事件就是更新一條訂單資料的狀態。

  在事件溯源系統中,領域物件的狀態是根據跟他相關的事件生成的。也就是說,如果我們不儲存業務狀態資料,那麼在每次處理一個訂單事件的時候,都要取出這個訂單的所有事件,然後根據這些事件生成當前的業務狀態資訊,然後再處理新的那個事件。

  所以,系統在處理每個事件的時候,只需要獲取這個事件所屬的物件相關的事件,而不需要獲取所有的事件。就好像我們在git中編輯一個檔案,就先獲取有關這個檔案的commit記錄,根據這些提交的commit事件,生成最新的檔案,然後在這個檔案上編輯,編輯完成後再提交一個新的commit事件。只不過,我們的git資源庫會將本地的最新的檔案狀態儲存下來,所以我們不需要每次都重新根據commit生成檔案。在git中我們可以對某一個版本打tag,在事件溯源模式中,我們也可以用類似的方式建立快照,將領域物件的當前狀態儲存成snapshot快照,這樣就不需要每次都獲取所有相關事件重新生成業務狀態了。

  事件溯源模式有很多有意思的特性,比如我們可以將整個系統看做是一個有版本管理功能的業務系統。根據上面說的開始重現功能,我們可以將系統的業務狀態充值到任何一個時間點。我們甚至可以在重現歷史的過程中,新增一些假想的業務資料,用於進行一些業務驗證。

  當然,事件溯源模式也有一些問題,如上面說的歷史重現,如果我們的系統需要依賴外部系統,那麼該系統在重新歷史的時候,其他的系統已經是最新的狀態,這就時空錯亂了,就會有錯誤。還有一個問題就是事件的資料結構的改變問題。如果事件的資料結構改變,但是歷史的事件中,相應的資料就會缺失,或者多餘。那麼系統就需要既能處理歷史版本的事件,也能處理新版本的事件。這無疑為我們的事件設計、和系統設計都帶來一定的困難。

  CQRS

  Command Query Responsibility Segregation (CQRS)簡單來說就是讀寫分離。也就是去操作使用的資料,跟寫操作使用的資料不是同一個資料。雖然從根本上講,實現CQRS模式,不一定要使用事件溯源模式來實現。但是一般情況下,當人們說CQRS的讀寫分離的時候,基本上都是通過事件溯源模式來實現的。

  要實現CQRS模式,一般都是通過事件溯源模式來進行資料更新操作,也就是所有的業務資料狀態的變更都通過事件來觸發。然後,對於每個事件,領域物件處理完該事件的同時,還有另一個事件處理器,根據這個事件將最新的業務狀態更新到資料庫中。然後,對於所有的讀操作,都從這個儲存業務狀態的資料庫中獲取。通過這種業務狀態的資料和事件資料的分離,相互之間不會出現資料的鎖,可以實現很高的吞吐量。

  可以發現,CQRS模式,看起來很優雅,但是實現起來往往比較複雜,如果沒有成熟的框架,而自己去實現,肯定會非常困難。

  合理使用這些模式

  至於說到如何合理的使用這些模式,首先要弄清楚這些模式。例如文中作者說到,有人說事件溯源模式給他們帶來了災難,每個事件都要處理兩次,(一個是更新領域物件,一次是更新read model),實際上是因為他們把事件溯源跟CQRS搞混了。使用事件溯源模式,不一定要用讀寫分離的方式使用。還有人說系統中大量的非同步通訊,導致了大量的複雜性(有些時候我們需要一個事件的處理結果)。但是事件溯源模式不一定非要是非同步發處理,這只是跟我們的實現有關,我們大可以在需要的地方使用同步的方式。

  我個人覺得,事件溯源+CQRS模式,確實是非常的優雅,我之前用過一點Axon框架,可以用來實現事件溯源+CQRS模式,而且這個框架的設計也很清晰,使用起來也比較容易。但是,這種方式確實會增加程式碼量,因為一個事件需要有2個處理函式,分別更新業務狀態資料和領域物件資料,還要定義一堆的命令和事件。除了Axon以外,也有一些別的框架,希望隨著事件驅動架構的應用越來越多,相應的框架也會越來越多,越來越成熟。