通過事件進行應用程式的設計是自20世紀80年代後期以來的一種實踐。我們可以在前端或後端的任何地方使用事件。當按下按鈕時,某些資料發生更改或執行某個後端動作。
但是事件究竟是什麼呢?我們什麼時候應該用它呢?缺點是什麼?
What/When/Why
當類或元件之間內聚性很高,它們的耦合度應該很低,也就是說當元件需要相互協作呼叫時,比如我們假設一個元件“A”需要觸發元件“B”中的一些邏輯,自然的方式是直接讓元件A呼叫元件B中的一個方法。但前提是A必須知道B的存在,這樣它們之間就是耦合的,A必須依賴於B了,這會使得系統更難以改變和維護。因此,這裡可以使用事件來防止這種直接呼叫的耦合。
此外,使用事件實現元件解耦也有其另外的,如果我們有一個只負責元件B的工作團隊,那麼他們則可能不需要與負責元件A的團隊進行交流,直接針對元件A中的邏輯改變在元件B中做出相對反應。兩個元件團隊可以獨立發展(banq注:微服務特點之一), 我們的應用系統變得更靈活。
即使在同一個元件團隊中,有時候我們不需要在同一請求/響應中立即執行一個動作的結果,只要非同步執行這個動作,比如傳送電子郵件。在這種情況下,我們可以立即向使用者返回響應,並以非同步方式傳送電子郵件,並避免讓使用者等待傳送電子郵件。
不過,如果我們不加區別地使用它,也有危險。我們會遇到邏輯流程的風險,這些邏輯流程在概念上是高度凝聚力的,但是通過採取脫鉤機制的事件連線在一起。換句話說,應該在一起的程式碼將被分開,並且難以跟蹤它的流程(類似於goto語句),不易於理解:可能是義大利麵一樣混在一起!
為了防止將我們的程式碼庫變成一大堆義大利麵條,我們應該將事件的使用限制在明確的情況下。根據我的經驗,有三種使用事件的情況:
(1).去耦元件
(2).執行非同步任務
(3).跟蹤狀態變化(稽核日誌)
1.去耦元件(微服務)
當元件A執行的邏輯需要觸發元件B的邏輯時,不要直接呼叫它,我們可以將觸發事件傳送到事件分派器。元件B將偵聽排程程式中的特定事件,並在事件發生時執行操作。
這意味著A和B都將取決於排程器和事件,但他們之間將不會知道對方存在,它們將被解耦。
理想情況下,排程員和事件都不應該在兩個元件之間存在:
(1)排程員應該是完全獨立於我們應用程式的庫,因此使用依賴管理系統安裝在通用位置。在PHP世界中,這是使用Composer等安裝在vendor資料夾中的東西。
(2) 事件是我們的應用程式的一部分,應該在兩個元件之間生存,元件之間通過事件進行通訊(結構上解耦,行為上耦合)。事件在元件之間共享,它是應用程式的核心部分。事件在DDD中屬於共享核心Shared Kernel的一部分。這樣,兩個元件都將依賴於共享核心,但彼此不會意識到。然而在單體Monolithic應用程式中,為方便起見,可以將其放在觸發事件的元件中。
。
DDD共享核心
[...]明確界定指定團隊同意分享的領域模型的一些子集。保持這個核心很小。[...]這個明確共享的東西有特殊的地位,如果沒有與其他團隊協商,不應該改變。
Eric Evans 2014, 領域驅動設計參考
2.執行非同步任務
有時候我們有一個我們想要執行的邏輯,但它可能需要相當長的時間來執行,我們不想讓使用者等待它完成。在這種情況下,希望將其作為非同步工作執行,並立即返回給使用者的訊息,通知他請求將在以後非同步執行。
例如,在網上商店下訂單可以同步完成,但傳送通知使用者的電子郵件可以進行非同步。
在這種情況下,我們可以做的是觸發一個將被排隊的事件,直到一個工作任務可以獲得這個事件並執行它,只要系統有資源。
在這些情況下,相關聯的邏輯是否在相同的有界環境中並不重要,無論哪種方式,邏輯都是去耦的。
3.跟蹤狀態變化(審計日誌)
以傳統的資料儲存方式,我們擁有一些資料的實體。當這些實體中的資料發生變化時,我們只需更新資料庫錶行以反映新值。
這裡的問題是,我們並不儲存這些值為什麼改變且什麼時候改變。
我們可以將這些改變的事件儲存在審計日誌中。
更多關於這個進一步的前景,在關於事件溯源的解釋。
事件模式
Martin Fowler確定了三種不同型別的事件模式:
(1)事件通知
(2)事件執行狀態轉移
(3)事件溯源Event Sourcing
所有這些模式共享相同的關鍵概念:
(1)事件是代表發生了一些事情(發生在某事之後);
(2)事件被廣播到正在監聽的任何程式碼(程式碼可以對事件做出反應)。
一. 事件通知
假設我們有一個具有明確定義的元件作為應用程式核心。理想情況下,這些元件是完全相互分離的,但是它們的一些功能需要在其他元件中執行一些邏輯。
最典型的情況如前所述:當元件A執行的邏輯需要觸發元件B的邏輯時,A不是直接去呼叫B,而是觸發事件將且傳送到事件排程程式。元件B將偵聽排程程式中的特定事件,並在事件發生時執行操作。
重要的是,這種模式的一個特徵是事件攜帶最少的資料。它只為聽眾提供足夠的資料,以便知道發生了什麼並執行其程式碼,通常只是實體ID,也可能是事件建立的日期和時間。
優點
(1)彈性更大:將事件排隊後,傳送方元件可以繼續執行其自己邏輯,即使由於錯誤發生,因為它們排隊等候,它們可以在錯誤被修復時被執行。
(2)降低延遲,如果事件排隊,使用者不需要等待該邏輯執行;
團隊可以獨立發展元件,使他們的工作更輕鬆,更快,更容易出現問題,更靈活;
缺點
(1)如果沒有使用標準,有可能變成一堆義大利麵條程式碼。
二. 事件執行狀態轉移
讓我們再次看看前面例子,一個具有明確定義的元件作為應用程式核心。如果A元件一些功能需要來自其他元件的資料。獲得該資料的最自然的方法是詢問其他元件,但這意味著被查詢元件必須提供查詢方法以供查詢元件使用,一次兩次修改增加無所謂,如果頻繁要求被查詢元件提供新的查詢方法,說明這兩個元件彼此耦合!
在元件之間共享資料的另一種方法是:擁有資料的元件觸發的更改事件時,該事件將攜帶全新更改後的資料。對該資料感興趣的元件將會監聽這些事件,從事件中獲得資料並儲存該資料的本地副本,然後進一步對這些全新資料做出反應。這樣,當他們需要外部資料時,他們其實在本地已經擁有它們,它們將不需要查詢其他元件,也不需要其他元件提供對應的查詢方法。
優點
(1)更大的彈性,查詢元件不依賴被查詢元件,如果被查詢元件變得不可用(因為有一個錯誤或遠端伺服器是不可達到的),查詢元件自身能正常工作,因為擁有了被查詢元件中主資料的本地資料;
(2)減少延遲,因為沒有遠端呼叫(假設被查詢元件是遠端的)訪問資料;
(3)我們不必擔心被查詢元件上的負載了,不用擔心它是否滿足來自所有其他查詢元件的查詢(特別是如果它是遠端元件);
缺點
(1)將有幾個相同資料的副本,雖然它們是隻讀副本,資料儲存在當下已經不是問題;
(2)更高的查詢元件的複雜性,因為它將需要邏輯來維護外部資料的本地副本,儘管這是非常標準的邏輯。主從一致性。
如果兩個元件在同一個程式中(同一個VM中、同一個主機內)執行,這種模式也許沒有必要,但即使這樣,它也可能很有趣,可以將其用於解耦和可維護性,或作為將這些元件分離到不同的微服務中工作做準備,也許在未來的某個時候我們能平滑升級到微服務。這一切都取決於我們目前的需求,未來的需求。
三. 事件溯源
我們假設一個實體處於一種初始狀態。作為一個實體,它有自己的身份,代表在現實世界中一個特定的事情,應用程式將其建模為實體。在其生命週期中,實體資料會發生變化,並且傳統上實體的當前狀態被簡單地作為表的一行記錄儲存在資料庫中。
(1)事務日誌
這在大多數情況下都是可以的,但如果我們需要知道實體是如何達到當前這個狀態,即我們想知道我們的銀行賬戶的貸方和借記發生的每筆金額,才能知道當前賬戶的餘額來歷,這在傳統只儲存當前狀態的方式下是不可能實現的,因為我們只儲存當前狀態!每次都是新的餘額狀態覆蓋了之前的狀態,比如當前餘額是10,覆蓋了之前餘額90,至於賬戶餘額怎麼剩餘10元呢?如果資料庫不儲存往來明細,你可能認為銀行系統出問題了。
儲存實體發生的事件,而不是儲存Entity狀態,我們專注於儲存實體狀態更改並從這些更改中計算實體狀態。每個狀態變化是一個事件,儲存在事件流中(即RDBMS中的一個表)。當我們需要實體的當前狀態時,我們從事件流中的所有事件中計算出它。
事件儲存成為真相的主要來源,系統狀態純粹源於它。對於程式設計師來說,最好的例子是版本控制系統。所有提交的日誌是事件儲存,源樹的工作副本是系統狀態。---2010年Greg Young, CQRS檔案
(2)如何刪除?
如果我們發現一個狀態改變(事件)是一個錯誤,我們不能簡單地刪除該事件,因為這會改變狀態更改歷史記錄,這將違反整個事情溯源的想法。相反,我們在事件流中建立一個事件,以反轉我們要刪除的事件。這個過程稱為反轉事務,不僅使實體返回到所需的狀態,而且留下了一個跟蹤,顯示物件在給定時間點處於該狀態。
不刪除資料也具有架構優勢。儲存系統成為只新增一個體繫結構,眾所周知,僅附加體系結構比更新架構更容易分發,因為要處理的鎖少得多。---2010年Greg Young, CQRS檔案
(3)快照
但是,當事件流中有許多事件時,計算實體狀態將是非常昂貴的,因此為了避免出現這種情況。每X個事件我們將在該時刻建立一個實體狀態的快照。這樣,當我們需要實體狀態時,我們只需要計算它到最後一個快照。我們甚至可以永久儲存更新實體的快照,這樣我們平衡了兩種世界(只儲存狀態和只儲存事件)。
(4)投影預測Projections
在事件採集中,我們也有一個投影的概念,即事件流中的事件的計算,從特定時刻開始。這意味著快照或實體的當前狀態符合預測的定義。但是在預測概念中最有價值的想法是,我們可以在特定時期分析實體的“行為”,這使我們能夠對未來作出有根據的猜測(即如果在過去的5年中,實體有8月份的活動增加,明年8月份可能會發生同樣的事情),這對業務來說可能是非常有價值的。
(5)利弊
事件溯源對於業務和開發過程都是非常有用的:
1.我們查詢這些事件,用於業務和開發,以瞭解使用者和系統行為(除錯);
2.我們還可以使用事件日誌重建過去的狀態,對於業務和開發來說都是有用的;
3.自動調整狀態以應對追溯變化,非常適合企業需要頻繁變化;
4.通過在重播時注入假想事件來探索其他歷史,令人敬畏。
但不是一切都是好訊息,要注意隱藏的問題:
1.外部更新
當我們的事件在外部系統中觸發更新時,但是我們又在重播事件以便建立投影,因此我們不想重新觸發這些事件。在這一點上,當我們處於“重播模式”時,我們可以簡單地禁用外部更新,也可以將該邏輯封裝在閘道器中。
另一個解決方案取決於實際問題,可能是將外部系統更新放入緩衝,在一段時間後執行,保證事件不會被重播時再進行。
2.外部查詢
當我們的事件使用對外部系統的查詢,如獲得股票評級,當我們重播事件以建立投影時會發生什麼?我們可能希望獲得與事件在第一次執行時所使用的相同的等級,也許是在幾年前。因此,遠端應用程式可以給我們這些值,或者我們需要將它們儲存在系統中,所以我們可以通過在閘道器中封裝該邏輯來模擬遠端查詢。
3.程式碼更改
Martin Fowler發現3種型別的程式碼更改:新功能,錯誤修復和時間邏輯。當在不同的時間點播放應該使用不同業務邏輯規則的事件時,真正的問題就出現。去年的稅收計算與今年不同。像往常一樣,條件邏輯可以使用,但它會變得凌亂,所以建議使用策略模式。
所以,我建議謹慎對待,儘可能遵循這些規則:
1.保持事態愚蠢,只關心狀態的變化,而不是如何被改變的。這樣我們可以安全地重播任何事件,並期望結果是一樣的,即使業務規則同時發生變化(儘管我們需要保留舊的業務規則,以便我們可以在重播過去的事件時應用它們);
2.與外部系統的互動不應該依賴於這些事件,這樣我們可以安全地重播事件,而不會重新觸發外部邏輯,我們不需要確保來自外部系統的回覆與最初的事件相同。
當然,像其他任何模式一樣,我們不需要在任何地方使用它,我們應該使用它在哪裡是有意義的,它給我們帶來了一個優勢,並解決了比建立更多的問題。
結論
這又是關於封裝,低耦合和高凝聚的問題。
事件可以平衡程式碼的可維護性、效能和擴充套件性,事件溯源也是系統資料可提供的可靠性和資訊。
然而,這是一條存在自身危險的道路,因為概念和技術的複雜性都會增加,而且任何一種的濫用都會產生災難性的後果。