這是一篇譯文,譯文首發於 事件驅動架構設計,轉載請註明出處!
這篇文章是 軟件架構演進 一個有關 軟件架構 系列文章中的一篇。這些文章,主要是我學習軟件架構、對軟件架構的思考及使用方法的記錄。相比於這個系列的前幾篇文章,本篇文章可能看來更有意義。php
採用設計驅動開發應用程序的實踐,能夠追溯到 1980 年左右。咱們能夠在前端或者後端採用事件驅動模型。好比點擊一個按鈕、數據變動或者某些後端服務被執行。html
可是究竟什麼纔是事件驅動呢?什麼時候使用事件驅動?它有沒有缺陷?前端
就像類和組件同樣咱們應當在編碼時實現高內聚低耦合。當須要組合使用組件時,好比 組件 A 須要觸發 組件 B 中的某些邏輯,咱們天然而然的會想到在 組件 A 中去直接調用 組件 B 實例中的方法。然而,若是 A 須要明確知道 B 的存在,那麼它們之間是耦合的,A 依賴於 B,這使得系統難以維護和迭代。事件驅動能夠 解決耦合 的問題。程序員
此外,採用事件驅動的另一個好處是,若是咱們有一個獨立的團隊開發 組件 B,他們能夠直接修改 組件 B 的業務邏輯而無需事先和研發 組件 A 的團隊進行溝通。各個組件能夠單獨迭代:咱們的系統更變得有組織性。數據庫
甚至,在同一個組建內,有時咱們的代碼須要在一個 request 和 response 週期內,做爲某個操做的結果被執行,可是又不須要當即被執行的相似處理。一個常見示例就是發送電子郵件。此時,咱們能夠直接響應用戶結果,而後以異步方式延遲發送一個電子郵件給用戶,這樣就避免了用戶等待發送郵件的時間。編程
不過,即便這樣處理依然存在風險。若是咱們胡亂使用事件驅動設計,咱們就有可能要承擔中斷業務邏輯的風險,由於這些業務邏輯具備概念上的高度內聚,卻採用瞭解耦機制將它們聯繫在一塊兒。換句話說,就是將本來須要組織在一塊兒的代碼強行分離,而且這樣難於定位處理流程(好比使用 goto 語句),來理解業務處理:這就變成了 麪條式的代碼[1]。後端
爲了防止咱們的代碼變成一堆複雜的邏輯,咱們應當在某些明確場景下使用事件驅動架構。就個人經驗來說,在如下 3 種場景下可使用事件驅動開發:設計模式
當組件 A 須要執行組件 B 中的業務邏輯,相比於直接調用,咱們能夠向事件分發器中發送一個事件。組件 B 經過監聽分發器中的特殊事件類型,而後當這類事件被觸發時去執行它。緩存
這意味着組件 A 和組件 B 都依賴於事件分發器和事件,而無需關注彼此實現:即完成它們的解耦。安全
理論上,分發器和事件應該處在不一樣的組件中:
共享內核
[...] 用明確的邊界指定團隊贊成共享的域模型的某些子集。保持這個內核很小。[...] 這個擁有特殊狀態的明確的共享機制,不得在未經團隊協商狀況下隨意修改。
Eric Evans 2014, Domain-Driven Design Reference
有時咱們會有一系列須要執行的業務邏輯,可是因爲它們須要耗費至關長的執行時間,因此咱們不想看到用戶耗費時間去等待這些邏輯處理完成。在這種狀況下,最好將它們做爲異步任務來運行,並當即向用戶返回一條信息,通知其稍後繼續處理相關操做。
好比,在網店下訂單能夠採用同步執行處理,可是發送通知郵件則採用異步任務去處理。
在這種狀況下,咱們所要作的是觸發一個事件,將事件加入到任務隊列中,直到一個 worker 進程可以獲取並執行這個任務。
此時,相關的業務邏輯是否處在同一個上下文中環境中並不重要,無論怎麼說,業務邏輯都是被執行了。
在傳統的數據存儲的方式中,咱們經過實體模型(entities)保存數據。當這些實體模型中的數據發生變化時,咱們只需更新數據庫中的行記錄來表示新的值。
這裏的問題是咱們沒法準確存儲數據的變動和修改時間。
咱們能夠經過審計日誌模型將包含修改的內容存入到事件裏。
在關於事件來源的知識,咱們會作進一步的闡述。
在實現事件驅動的架構時,一個常見的爭議是到底是使用 監聽器(listener) 仍是 訂閱者(Subscriber),這裏談談個人見解:
Martin Fowler 定義了 3 種事件模式:
這三種模式核心是同樣的:
假設,有一個應用在內核(core)中定義了一些組件。理想狀況下,這些組件是徹底分離的,可是它們的一些功能須要在其餘組件中去執行一些邏輯。
這是最典型的應用場景,前面已經講過:當組件 A 執行時,須要觸發組件 B 中的邏輯時,這裏能夠去觸發一個事件將其發送到事件分發器中,而不是直接調用。組件 B 經過監聽分發器中的這類事件,當有事件觸發時去執行這個事件。
須要注意的是,這個模式的一個特徵是 事件自己攜帶的數據非量常少。它只攜帶足夠的數據,以便監聽器知道發生了什麼,並執行它們的代碼,數據一般是實體模型的 ID,可能還有事件建立的日期和時間。
優勢
缺點
仍是以前那個在內核中定義了一些組件的應用。此次,多於一些功能須要使用其它組件中的數據。獲取數據的最天然方式是從其它組件中查詢出數據,可是這也意味着這個組件知道被查詢組件的存在:這樣兩個組件就偶合在一塊兒了!
實現數據共享的另外一種方法是,當數據在所屬組件中被變動時,觸發一個事件。這個事件攜帶新版本中的全部數據。對該數據感興趣的組件能夠監聽這類事件,並依據數據存儲中的數據進行處理。這樣當組件之間須要外部數據時,他們也可以獲取本地副本,而無需從其它組件中查詢。
優勢
缺點
若是兩個組件都在同一個進程中,可以快速的實現組件間通訊,那麼實現這種設計模式可能就沒那麼必要了。不過爲了實現組件分離或可維護性,或在將來的計劃中將組件封裝進不一樣的微服務中使用這種模式。全部的一切取決於現有需求和計劃,以及咱們但願(或須要)將系統解耦到什麼程度。
假設,如今有一個剛剛初始化的實體(Entity)。做爲實體,它有本身的標識(identity),它對應現實世界中的某一事物,在程序中就是模型。在整個生命週期內,數據庫僅僅簡單的保存實體的當前狀態。
多數場景下,這種存儲方式是可行的,但若是咱們須要知道實體究竟如何到達當前這個狀態(好比,咱們想知道銀行帳戶的貸方和借方)。這時候因爲咱們僅存儲當前狀態,可能就沒法實現這種需求了。
使用事件溯源模式替代實體狀態存儲,咱們關注實例狀態的 變動 並 依據變動計算出實體狀態。每一個狀態的變化都是一個事件,被存儲到事件流中(如 RDBMS 中的表)。當咱們須要獲取實體的當前狀態是,咱們經過計算這個事件的全部事件流來完成。
事件存儲做爲結果的主要來源,系統狀態也單純的轉變成了它的派生結果。對程序員來講,最好的例子是版本控制系統。全部的提交日誌就是事件存儲,當前源代碼樹的工做副本就是系統的狀態。
Greg Young 2010, [CQRS Documents](https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf)
若是如今存在一個錯誤的狀態變動(event),咱們不能簡單的將其刪除由於這樣會改變狀態的歷史記錄,這就與事件溯源的設計初衷背道而馳了。替代的方法是,咱們在事件流裏建立一個新的事件,咱們將但願刪除的事件回退(reverses)到以前的狀態。這個過程稱之爲事務回退,這個操做不只將實體恢復到指望的狀態,還留下記錄表名這個實體在給定的時間節點所處的狀態。
不刪除數據也有架構上的收益。存儲系統成爲一種僅添加的架構,衆所周知,僅添加的架構比起可更新架構更容易部署,由於它要處理的鎖要少得多。
Greg Young 2010, [CQRS Documents](https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf)
不過,當在一個事件流中包含不少的事件時,計算實體狀態則會變的代價高昂,還會嚴重影響性能。爲了解決這個問題,每當產生 X 條事件時,咱們將在那個時間點建立實體狀態的快照。甚至,咱們能夠保存這個實體的永久更新過的快照,這樣咱們就能同時擁有兩個最優的平行世界。
在事件溯源中咱們還引入了 投影(projection) 的概念,它是必定時間範圍內基於事件流計算後的事件結果。這就是快照,或者說實體的當前狀態,這就是投影的定義。可是在 投影 這個概念中最有價值的是,咱們能夠經過分析特定時間內的實體「行爲」,實現對將來的行爲做出預測(好比,在過去 5 年裏實體模型都在 8 月份增長了活動量,那麼它頗有可能在明年 8 月份產生一樣的行爲)。這對企業來講是一個頗有價值的能力。
事件溯源在商業和軟件開發過程這兩方面很是有用:
然而,並不是一切都如此美好,警戒以下問題:
當事件在外部系統中觸發更新時,咱們不但願在回放事件以建立投影時從新觸發這些事件。此時,咱們只需在 「回放模式」中禁用外部更新,能夠將這個邏輯封裝到網關裏實現。
另外一種解決方案依賴於實際的問題,能夠將更新緩存(buffer)到外部系統,在一段時間後執行更新,這時能夠安全地假設事件不會回放。
當在外部系統中使用查詢來檢索咱們的事件時,好比獲取股票債券評級,當咱們回放事件來建立投影時會發生什麼呢? 咱們可能想要獲得與事件第一次運行時相同的評分,這也許是幾年前生成的。所以,遠程應用能夠給咱們這些值,或者咱們須要將它們存儲在咱們的系統中,這樣咱們就能夠經過封裝網關中的邏輯來模擬遠程查詢。
Martin Fowler 定義了 3 種類型的代碼變動:新特性(new features),bug 修復和臨時邏輯。真正的問題出如今回放事件時,這些事件應該在不一樣的時間點使用不一樣的業務邏輯規則,好比,去年的稅收計算就與今年的不一樣。一般狀況下,可使用條件語句,可是這回使邏輯變得混亂,因此建議使用策略模式。
個人建議是謹慎使用這個模式,通常我會盡可能遵循以下原則:
固然,和其它模式同樣,並不是任什麼時候候均可以使用它,當使用比不適用帶來更多收益時,咱們應該去使用這種模式。
事件驅動架構核心在於封裝、高內聚和低耦合。
事件驅動能夠提高代碼的可維護性、性能和業務增加的需求,可是,經過事件溯源模式,還能提升系統數據的可靠性。
不過,事件驅動一樣存在弊端,由於不管是概念上的複雜度仍是技術上的複雜度都增長了,當它被濫用時將致使災難性的後果。
2005 • Martin Fowler • Event Sourcing
2006 • Martin Fowler • Focusing on Events
2010 • Greg Young • CQRS Documents
2014 • Greg Young • CQRS and Event Sourcing – Code on the Beach 2014
2014 • Eric Evans • Domain-Driven Design Reference
2017 • Martin Fowler • What do you mean by 「Event-Driven」? 中譯 中譯2
2017 • Martin Fowler • The Many Meanings of Event-Driven Architecture
[1] 麪條式代碼(Spaghetti code)是軟件工程中反面模式的一種 (1),是指一個源代碼的控制流程複雜、混亂而難以理解 (2),尤爲是用了不少 GOTO、例外、線程、或其餘無組織的分支。其命名的緣由是由於程序的流向就像一盤面同樣的扭曲糾結。麪條式代碼的產生有許多緣由,例如沒有經驗的程序設計師,及已通過長期頻繁修改的複雜程序。結構化編程可避免麪條式代碼的出現。這樣,當咱們須要獲取實體狀態時,只須要計算最後一個快照便可。