事件驅動架構設計

這是一篇譯文,譯文首發於 事件驅動架構設計,轉載請註明出處!

這篇文章是 軟件架構演進 一個有關 軟件架構 系列文章中的一篇。這些文章,主要是我學習軟件架構、對軟件架構的思考及使用方法的記錄。相比於這個系列的前幾篇文章,本篇文章可能看來更有意義。php

採用設計驅動開發應用程序的實踐,能夠追溯到 1980 年左右。咱們能夠在前端或者後端採用事件驅動模型。好比點擊一個按鈕、數據變動或者某些後端服務被執行。html

可是究竟什麼纔是事件驅動呢?什麼時候使用事件驅動?它有沒有缺陷?前端

是什麼、何時用、爲何用(What / When / Why)

就像類和組件同樣咱們應當在編碼時實現高內聚低耦合。當須要組合使用組件時,好比 組件 A 須要觸發 組件 B 中的某些邏輯,咱們天然而然的會想到在 組件 A 中去直接調用 組件 B 實例中的方法。然而,若是 A 須要明確知道 B 的存在,那麼它們之間是耦合的,A 依賴於 B,這使得系統難以維護和迭代。事件驅動能夠 解決耦合 的問題。程序員

此外,採用事件驅動的另一個好處是,若是咱們有一個獨立的團隊開發 組件 B,他們能夠直接修改 組件 B 的業務邏輯而無需事先和研發 組件 A 的團隊進行溝通。各個組件能夠單獨迭代:咱們的系統更變得有組織性數據庫

甚至,在同一個組建內,有時咱們的代碼須要在一個 request 和 response 週期內,做爲某個操做的結果被執行,可是又不須要當即被執行的相似處理。一個常見示例就是發送電子郵件。此時,咱們能夠直接響應用戶結果,而後以異步方式延遲發送一個電子郵件給用戶,這樣就避免了用戶等待發送郵件的時間。編程

不過,即便這樣處理依然存在風險。若是咱們胡亂使用事件驅動設計,咱們就有可能要承擔中斷業務邏輯的風險,由於這些業務邏輯具備概念上的高度內聚,卻採用瞭解耦機制將它們聯繫在一塊兒。換句話說,就是將本來須要組織在一塊兒的代碼強行分離,而且這樣難於定位處理流程(好比使用 goto 語句),來理解業務處理:這就變成了 麪條式的代碼[1]。後端

爲了防止咱們的代碼變成一堆複雜的邏輯,咱們應當在某些明確場景下使用事件驅動架構。就個人經驗來說,在如下 3 種場景下可使用事件驅動開發:設計模式

  1. 實現組件的解耦
  2. 執行異步任務
  3. 跟蹤狀態的變化(審計日誌(audit log))

1 實現組件的解耦(To decouple components)

當組件 A 須要執行組件 B 中的業務邏輯,相比於直接調用,咱們能夠向事件分發器中發送一個事件。組件 B 經過監聽分發器中的特殊事件類型,而後當這類事件被觸發時去執行它。緩存

這意味着組件 A 和組件 B 都依賴於事件分發器和事件,而無需關注彼此實現:即完成它們的解耦。安全

理論上,分發器和事件應該處在不一樣的組件中:

  • 分發器應當是獨立於應用的組件庫,而後使用依賴管理工具安裝到系統中。在 PHP 裏,咱們使用 Composer 將其安裝到 vendor 目錄。
  • 對於事件來講,它是咱們應用的一部分,但須要獨立於這兩個組件以外,這樣使得組件之間相互獨立。而且事件在組件之間實現共享,它是應用核心的不可分割的一部分。事件就是 DDD(領域驅動設計) 調用 共享內核(Shared Kernel) 的一部分。這樣,這些組件就依賴於共享內核,而無需知道彼此的存在。不過在單個系統中,爲了方便咱們也能夠在組件內去觸發事件。
共享內核
[...] 用明確的邊界指定團隊贊成共享的域模型的某些子集。保持這個內核很小。[...] 這個擁有特殊狀態的明確的共享機制,不得在未經團隊協商狀況下隨意修改。
Eric Evans 2014, Domain-Driven Design Reference

2 執行異步任務(To perform async tasks)

有時咱們會有一系列須要執行的業務邏輯,可是因爲它們須要耗費至關長的執行時間,因此咱們不想看到用戶耗費時間去等待這些邏輯處理完成。在這種狀況下,最好將它們做爲異步任務來運行,並當即向用戶返回一條信息,通知其稍後繼續處理相關操做。

好比,在網店下訂單能夠採用同步執行處理,可是發送通知郵件則採用異步任務去處理。

在這種狀況下,咱們所要作的是觸發一個事件,將事件加入到任務隊列中,直到一個 worker 進程可以獲取並執行這個任務。

此時,相關的業務邏輯是否處在同一個上下文中環境中並不重要,無論怎麼說,業務邏輯都是被執行了。

3. 跟蹤狀態的變化(審計日誌(audit log))

在傳統的數據存儲的方式中,咱們經過實體模型(entities)保存數據。當這些實體模型中的數據發生變化時,咱們只需更新數據庫中的行記錄來表示新的值。

這裏的問題是咱們沒法準確存儲數據的變動和修改時間。

咱們能夠經過審計日誌模型將包含修改的內容存入到事件裏。

在關於事件來源的知識,咱們會作進一步的闡述。

監聽器 vs 訂閱者(Listeners Vs Subscribers)

在實現事件驅動的架構時,一個常見的爭議是到底是使用 監聽器(listener) 仍是 訂閱者(Subscriber),這裏談談個人見解:

  1. 事件監聽器 僅對一種事件做出響應,同時可以使用多種方法處理事件。所以,咱們應該依據事件名來命令監聽器,好比,假設咱們定義一個「UserRegisteredEvent」事件,咱們就應當實現一個「UserRegisteredEventListener」監聽器,這樣咱們就可以很輕易的知道監聽器在監聽什麼事件,而無需經過查看文件內的實現。而後就是對事件的處理方法(反應)應該正確反映方法的功能,好比「notifyNewUserAboutHisAccount()」和「notifyAdminThatNewUserHasRegistered()」。這種模式可以應付大多數的使用場景,由於這樣不只可以保證監聽器足夠小巧,並且知足專一於響應特定事件的單個職能原則。此外,若是咱們是一個組合架構,每一個組件(若有有必要)都須要定義一個能夠在不一樣位置觸發的事件監聽器。
  2. 事件訂閱者(Event Subscriber) 支持多種事件和事件處理方法。訂閱者模式命名會更麻煩一點,由於它不只僅處理一種事件,不過訂閱者依然須要遵循單一職責原則,因此訂閱者命名也須要可以反映其意圖。使用事件訂閱者並不常見,特別是在組件中,由於它可以輕易的打破單一職責原則。實現訂閱者的一個很是適合的使用場景是管理事務,具體來說咱們有個名爲「RequestTransactionSubscriber」訂閱者,它等待諸如「RequestsReceivedEvent」、「ResponseSentEvent」和「KernelExceptionEvent」事件,並將其綁定到事務的啓動、提交和回滾處理,經過在它們內部定義「startTransaction()」、「finishTransaction()」和「rollbackTransaction()」方法。這裏雖然一個訂閱者可以對多個事件做出響應,但依然僅關注管理請求事務中的某一個職能。

模式

Martin Fowler 定義了 3 種事件模式:

  • 事件通知
  • 事件承載狀態轉移
  • 事件溯源

這三種模式核心是同樣的:

  1. 事件發生則表示發生了一些事情(事件發生在這些事情後);
  2. 事件被廣播到它的監聽代碼中(多個監聽程序能夠共同處理一個事件)。

事件通知(Event Notification)

假設,有一個應用在內核(core)中定義了一些組件。理想狀況下,這些組件是徹底分離的,可是它們的一些功能須要在其餘組件中去執行一些邏輯。

這是最典型的應用場景,前面已經講過:當組件 A 執行時,須要觸發組件 B 中的邏輯時,這裏能夠去觸發一個事件將其發送到事件分發器中,而不是直接調用。組件 B 經過監聽分發器中的這類事件,當有事件觸發時去執行這個事件。

須要注意的是,這個模式的一個特徵是 事件自己攜帶的數據非量常少。它只攜帶足夠的數據,以便監聽器知道發生了什麼,並執行它們的代碼,數據一般是實體模型的 ID,可能還有事件建立的日期和時間。

  • 優勢

    • 更健壯(Greater resilience),若是加入隊列的事件可以在源組件中執行,但在其它組件中因爲 bug 致使其沒法執行(因爲將其加入到隊列任務中,它們能夠在 bug 修復後再執行);
    • 減小延遲,當用戶無需等待全部的邏輯都執行完成時,能夠將這類工做加入到事件隊列;
    • 可以讓組件的研發團隊獨立開發,加快項目進度、下降功能難度、減小問題發生而且更有組織性;
  • 缺點

    • 若是沒有合理使用,可能時咱們的代碼變成苗條式代碼。

事件承載狀態轉移(Event-Carried State Transfer)

仍是以前那個在內核中定義了一些組件的應用。此次,多於一些功能須要使用其它組件中的數據。獲取數據的最天然方式是從其它組件中查詢出數據,可是這也意味着這個組件知道被查詢組件的存在:這樣兩個組件就偶合在一塊兒了!

實現數據共享的另外一種方法是,當數據在所屬組件中被變動時,觸發一個事件。這個事件攜帶新版本中的全部數據。對該數據感興趣的組件能夠監聽這類事件,並依據數據存儲中的數據進行處理。這樣當組件之間須要外部數據時,他們也可以獲取本地副本,而無需從其它組件中查詢。

  • 優勢

    • 更健壯(Greater resilience),由於查詢組件在被查詢組件不可用狀況下(或者因爲 bug 或遠程服務器不可用時)依然可用;
    • 減小延遲,由於無需遠程調用(當被查詢組件爲遠程服務時)來獲取數據;
    • 無需擔憂被查詢組件的負載(尤爲是遠程組件)
  • 缺點

    • 儘管如今數據存儲已經再也不是問題根源,依然會保存多個只讀的數據副本;
    • 增長查詢組件的複雜度,即便處理邏輯符合規範它也須要額外處理和維護外部數據的本地副本業務邏輯。

若是兩個組件都在同一個進程中,可以快速的實現組件間通訊,那麼實現這種設計模式可能就沒那麼必要了。不過爲了實現組件分離或可維護性,或在將來的計劃中將組件封裝進不一樣的微服務中使用這種模式。全部的一切取決於現有需求和計劃,以及咱們但願(或須要)將系統解耦到什麼程度。

事件溯源(Event-Sourcing)

假設,如今有一個剛剛初始化的實體(Entity)。做爲實體,它有本身的標識(identity),它對應現實世界中的某一事物,在程序中就是模型。在整個生命週期內,數據庫僅僅簡單的保存實體的當前狀態。

事務日誌(Transaction log)

多數場景下,這種存儲方式是可行的,但若是咱們須要知道實體究竟如何到達當前這個狀態(好比,咱們想知道銀行帳戶的貸方和借方)。這時候因爲咱們僅存儲當前狀態,可能就沒法實現這種需求了。

使用事件溯源模式替代實體狀態存儲,咱們關注實例狀態的 變動依據變動計算出實體狀態。每一個狀態的變化都是一個事件,被存儲到事件流中(如 RDBMS 中的表)。當咱們須要獲取實體的當前狀態是,咱們經過計算這個事件的全部事件流來完成。

事件存儲做爲結果的主要來源,系統狀態也單純的轉變成了它的派生結果。對程序員來講,最好的例子是版本控制系統。全部的提交日誌就是事件存儲,當前源代碼樹的工做副本就是系統的狀態。

Greg Young 2010, [CQRS Documents](https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf)

刪除(Deletions)

若是如今存在一個錯誤的狀態變動(event),咱們不能簡單的將其刪除由於這樣會改變狀態的歷史記錄,這就與事件溯源的設計初衷背道而馳了。替代的方法是,咱們在事件流裏建立一個新的事件,咱們將但願刪除的事件回退(reverses)到以前的狀態。這個過程稱之爲事務回退,這個操做不只將實體恢復到指望的狀態,還留下記錄表名這個實體在給定的時間節點所處的狀態。

不刪除數據也有架構上的收益。存儲系統成爲一種僅添加的架構,衆所周知,僅添加的架構比起可更新架構更容易部署,由於它要處理的鎖要少得多。

Greg Young 2010, [CQRS Documents](https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf)

快照(Snapshots)

不過,當在一個事件流中包含不少的事件時,計算實體狀態則會變的代價高昂,還會嚴重影響性能。爲了解決這個問題,每當產生 X 條事件時,咱們將在那個時間點建立實體狀態的快照。甚至,咱們能夠保存這個實體的永久更新過的快照,這樣咱們就能同時擁有兩個最優的平行世界。

event snapshots

投影(Projections)

在事件溯源中咱們還引入了 投影(projection) 的概念,它是必定時間範圍內基於事件流計算後的事件結果。這就是快照,或者說實體的當前狀態,這就是投影的定義。可是在 投影 這個概念中最有價值的是,咱們能夠經過分析特定時間內的實體「行爲」,實現對將來的行爲做出預測(好比,在過去 5 年裏實體模型都在 8 月份增長了活動量,那麼它頗有可能在明年 8 月份產生一樣的行爲)。這對企業來講是一個頗有價值的能力。

同意 vs 反對(Pros and cons)

事件溯源在商業和軟件開發過程這兩方面很是有用:

  • 經過查詢這些事件,有助於商業和開發時理解用戶和系統行爲(調試);
  • 咱們還可使用事件日誌來重建過去的狀態,這對商業和開發都頗有用;
  • 自動調整狀態以追溯變動狀況,在商業上意義重大;
  • 在回放(replay)時,經過輸入預設事件探索已有歷史記錄,在商業上一樣有意義。

然而,並不是一切都如此美好,警戒以下問題:

  • 外部更新(External updates)

當事件在外部系統中觸發更新時,咱們不但願在回放事件以建立投影時從新觸發這些事件。此時,咱們只需在 「回放模式」中禁用外部更新,能夠將這個邏輯封裝到網關裏實現。

另外一種解決方案依賴於實際的問題,能夠將更新緩存(buffer)到外部系統,在一段時間後執行更新,這時能夠安全地假設事件不會回放。

  • 外部查詢(External Queries)

當在外部系統中使用查詢來檢索咱們的事件時,好比獲取股票債券評級,當咱們回放事件來建立投影時會發生什麼呢? 咱們可能想要獲得與事件第一次運行時相同的評分,這也許是幾年前生成的。所以,遠程應用能夠給咱們這些值,或者咱們須要將它們存儲在咱們的系統中,這樣咱們就能夠經過封裝網關中的邏輯來模擬遠程查詢。

  • 代碼變動(Code Changes)

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

補充資料

什麼是事件溯源

淺談命令查詢職責分離 (CQRS) 模式

Command 與 Query 分離(CQS)

註解

[1] 麪條式代碼(Spaghetti code)是軟件工程中反面模式的一種 (1),是指一個源代碼的控制流程複雜、混亂而難以理解 (2),尤爲是用了不少 GOTO、例外、線程、或其餘無組織的分支。其命名的緣由是由於程序的流向就像一盤面同樣的扭曲糾結。麪條式代碼的產生有許多緣由,例如沒有經驗的程序設計師,及已通過長期頻繁修改的複雜程序。結構化編程可避免麪條式代碼的出現。這樣,當咱們須要獲取實體狀態時,只須要計算最後一個快照便可。

原文

Event-Driven Architecture

相關文章
相關標籤/搜索