事件溯源:投影或投射模式 -Kacper Gunia

banq發表於2019-11-13

投影是事件源中使用的核心模式之一。ES所瞭解的是,作為一系列事件將應用程式中正在發生的更改持久化。然後,該事件序列(也稱為流)可用於重建當前狀態,以便可以處理任何後續請求。
從理論上講,我們可以僅在事件流中停下來做所有事情。不幸的是,這很快變得非常低效。通常,讀取(查詢)發生的次數比寫入(命令)發生的次數多。
如果我們檢視傳統的銀行業務示例,則可以查詢帳戶中曾經發生的所有交易,然後從更改歷史記錄中得出當前餘額。不幸的是,讀取成百上千個事件將意味著我們將花費大量時間進行IO,然後還要花費一些時間來計算當前餘額。
相反,如果我們可以預先計算當前餘額並將值儲存在某個地方,則可以更快地回答該查詢。您可以將其視為例項化檢視或快取的一種形式。使用這種方法帶來了CQRS(命令查詢責任隔離)模式,該模式圍繞以下思想定義:您可以使用兩個不同的模型來讀取(讀取模型)和寫入(寫入模型)資訊。

投影的定義
投影定義:從事件流中得出的當前狀態是什麼?
正如格雷格·揚(Greg Young)解釋的那樣,投影不過是事件序列的left-fold,這是表達定義的一種有效方式。在Scala中,foldLeft是一個高階函式,其定義(簡化)為:

trait Traversable[+A] {
    def foldLeft[B](z: B)(op: (B, A) => B): B
}


對於指定的事件A的traversable,left-fold需要一個初始狀態B和一個函式,用來使用事件A運算op該更新當前狀態B,**然後返回一個新的狀態B **。op運算函式將在從左到右遍歷的每個元素上被呼叫,並且更新的狀態將被傳遞。

(原文以scala為案例,Java案例見這裡)
這是一個賬戶進出事件集合類,計算賬戶餘額:

/**
     * Calculates the balance by summing up the values of all activities within this window.
     */
    public Money calculateBalance(AccountId accountId) {
        Money depositBalance = activities.stream()
                .filter(a -> a.getTargetAccountId().equals(accountId))
                .map(Activity::getMoney)
                .reduce(Money.ZERO, Money::add);

        Money withdrawalBalance = activities.stream()
                .filter(a -> a.getSourceAccountId().equals(accountId))
                .map(Activity::getMoney)
                .reduce(Money.ZERO, Money::add);

        return Money.add(depositBalance, withdrawalBalance.negate());
    }


使用Java stream實現最新餘額狀態計算。

投射持久化

1. 投射結果儲存在記憶中
維護投影狀態的最簡單方法是在服務啟動時讀取事件的整個流,然後將當前狀態儲存在記憶體中。任何獲取當前狀態的查詢都將盡快得到答覆。當事件數量很少(因此可以快速重播)並且可以承受該服務在事件失敗時可以重播事件的位置時,此方法非常適合原型製作。

2. 投射結果儲存SQL資料庫中
一種傳統的儲存投影狀態的方式,可能是最常用的一種方式。處理事件後,我們將最新狀態儲存在表中,然後在需要時可以在接收到任何後續事件時對其進行查詢或更新。
用於構建投影並將其儲存在SQL儲存中的一種啟發式方法是,應將其設計為回答我們要詢問的問題。如果以這種方式實現,那麼任何獲取狀態的查詢都可以非常快速地得到答覆。當涉及聯接或聚合查詢時,可能表明當前的投影設計不是最佳的,或者它在多個用例中使用。
需要注意的陷阱之一是投影之間的依賴性。如果兩個不同的投影相互依賴,那麼以後需要時就很難重建它們。不幸的是,如果使用傳統的SQL儲存,此陷阱通常很容易掉下來,因為聯接非常容易執行。可能會很想嘗試由其他投影建立的表以獲取當前在主投影中不可用的資料。這種方法的主要問題是,每個預測都有其自己的生命週期,並且傳播到其中一個的更改可能還沒有到達另一個。然後,這可能導致比賽條件,錯誤或其他難以解釋的行為。
使用SQL構成的另一個挑戰是誘惑為其實體建立第三正規化模型。這通常是由於需要能夠執行系統將來可能需要回答的即席查詢。相反,我們應該集中精力瞭解什麼是我們需要支援的實際用例,然後建立一組有助於我們實現這些目標的預測。

3. 投射結果在NoSQL
市場上NoSQL解決方案數量的增加極大地提高了開發人員的意識,即沒有“一刀切”的解決方案來解決查詢問題。將事件流投射到任何後端的能力使我們能夠改善使用者體驗並簡化應用程式程式碼。一個示例可以是將投影狀態儲存在搜尋引擎,時間序列資料庫或分散式鍵值儲存中,這些儲存將能夠支援查詢資料的最佳方式。

4. 投射結果在檔案
檔案系統是另一種選擇,尤其是任何雲物件儲存都可以被視為投影資料的儲存庫。可能的示例之一是將資料儲存為json或xml檔案,以便應用程式客戶端可以直接使用它們。

保持最新狀態
寫入狀態儲存後,需要更新和儲存投影的狀態-它可以透過兩種方式發生:

  • 同步 -與寫入事件流相同的事務中。這種方法通常非常受限制,因為它假定事件與投影資料儲存在同一資料庫中。它還不容易擴充套件,並且存在其他操作問題(例如可能無法實現或難以同步的重播)。從好的方面來說,它降低了操作複雜性,並且還允許假定投影狀態被立即更新。根據經驗,我們可以說這種方法對於原型設計很有用,但對於系統的生產版本卻沒什麼用。
  • 非同步 -將事件寫入事件儲存後,事件會傳遞到投影。根據可用的基礎結構和擴充套件需求,這可以以基於推或拉的方式發生。由於更新是非同步的,因此我們將不得不處理由投影儲存的最終資料一致性以及交付保證。從好的方面來看,現在可以將預測與主要事務寫入分離,並且可以根據需要進行獨立縮放,重放和監視。


在現實生活中,投影實現往往包含兩個部分:
  • 一個庫,允許查詢或儲存狀態
  • 一個投影器:也就是事件處理程式,它知道如何更新或建立的狀態。

相關文章