跳到主要內容

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

周蕾 (ArcBlock 后端工程师)
ArcBlockcqrs

作者 周蕾(ArcBlock 後端工程師)

最近幾年,在 DDD 的領域,我們經常會看到 CQRS 架構的概念,CQRS 是查詢職責分離模式(Command Query Responsibility Segregation)的縮寫。正好這些日子 ArcBlock 的後端服務有考慮使用 CQRS 的架構,所以今天和大家一起分享一下最近的研讀收穫。今天文章會從 Event Sourcing 出發介紹 CQRS,以及透過 Commanded(Elixir 的函式庫),一起看一看如何遵循 ES/CQRS 的概念開發應用程式。

什麼是 Event Sourcing(事件溯源)?

Event Sourcing 本質來說是保存了發生事件的本身,而不是當前事物的狀態。在 Event Sourcing 的概念裡,Event 作為既定發生之後的事情,也是最小的單位。 例如:

圖1

Event Sourcing 的工作模式:在下面這條資料流裡面,由 4 個發生的事件(event)組成,進而每一次改變當前的狀態,同時事件們的相對順序也是我們需要保證的。

圖1

我們會得到這樣的總結:

Sn = apply(Sn-1, En) 或者 Sn = reduce(E, S0, apply)

其中:(S:state,E:Event)

現在我們可以發現 Event Sourcing 的優點:

  • 每個狀態發生的改變都有完備的日誌記錄,可追溯
  • 優化了寫入操作,提高了效能

我們身邊的 Event Sourcing

  • 每個程式設計師每天都離不開的 GitHub。在 Git 的世界裡,Events(事件)是 Commits,State(狀態)是檔案系統。
  • Blockchain 每次保存的是 transaction 而不是一個現在的狀態,從這個角度出發,Events(事件)是 transaction,State(狀態)是使用者的帳戶資訊。
  • WAL:是 Write-ahead logging,在資料寫入到資料庫之前,先寫入到日誌,再將日誌記錄變更到儲存器中。Events(事件)是每一個操作,State(狀態)是資料庫。

對於 Event Sourcing 來說,想做查詢(query)怎麼辦?

試想一下,在一個銀行系統裡面,如果我們想要查詢帳戶餘額在 1000 元以上的使用者,那我們難道需要把每個帳戶按照 Sn = reduce(E, S0, apply) 這個公式再重新計算一遍嗎?如果我們考慮用一個 DataStore 來保存 Event,再用另外一個 DataStore 去專門為 Query 提供資料,同時兩個 Datastore 透過傳送訊息進行資訊同步,如何?CQRS 某種程度上就解決了這樣的問題。

CQRS 是什麼?

CQRS 全稱是 Command Query Rsponsibility Segregation,將應用程式分為兩部分:命令端(Command)和查詢端(Query)。命令端處理程式建立、更新和刪除請求,並在資料變更時發出事件。查詢端透過執行查詢來處理查詢,並且透過訂閱資料變更時發出的事件流而保持最新。CQRS 使用分離的介面將資料查詢操作(Queries)和資料修改操作(Commands)分離開來,這也意味著在查詢和更新過程中使用的資料模型也是不一樣的。這樣讀取和寫入邏輯就隔離開來了。

圖1

CQRS 裡面的一些概念:

  • Command(命令):不返回任何結果,通過驗證後會改變物件的狀態。
  • Query(查詢):有返回結果,但是不會改變物件的狀態。
  • Aggregate(聚合):保存狀態,處理 command,和改變狀態。
  • Event Store:儲存 Events。

怎麼遵循 CQRS 模式建立應用程式?

首先我們會基於 Commanded,一個遵循 CQRS/ES 模式實作 Command side 的 Elixir 函式庫。

1. Commands

Commands 是使用者傳送給應用程式的指令,表示使用者的一種請求,當然請求是有可能失敗的,例如想在餘額只有 10 的帳戶裡面取出 1000 元這樣的操作。每一個指令對應一個 module,然後使用 defstruct 定義欄位,命名方式是 MineCoin,動名結構。

    defmodule MineCoin do
        defstruct [
          :account_id,
          :nonce
        ]
    end

2. Events

Events 是由 Command 產生,最終導致狀態改變。最終會在 eventstore 裡面序列化儲存,可以用於日後想要恢復狀態。命名方式相比於 Command 來說發生了變化,CoinMined,以過去式表達一種過去發生的事實。

    defmodule CoinMined do
        defstruct [
            :account_id,
            :nonce
        ]
    end

3. Aggregates

Aggregates 作為接受、處理 Command,產生或者引起對應事件的發生,以及一些改變狀態的處理器。

裡面包含兩個函數:execute 方法用來加入我們驗證 Command 的一些邏輯,輸入是狀態和 command,如果成功,輸出就是 Event。 apply 函數用來更改狀態,注意這裡的物件已經是產生出來的 event。

  @spec execute(state, command)
      ::{ok, [event]}
      | {:error, term()}

  @spec apply(state, event) ::state

現在我們有了 Command、Event、Aggregates……

那我們還需要一個派遣的角色幫助我們把 Command 導向對應的 Aggregates。Commanded 函式庫提供了 Router:

    defmodule Coins.Router do
        use Commanded.Commands.Router

        alias Coins.Account
        alias Coins.Commands, as: C

        dispatch(
        [
            C.MineCoin
        ],
            to: Account
        )
    end

最後我們使用 Commanded 推薦的 EventStore,它是基於 PostgreSQL 作為儲存引擎,來保存 Events。

現在可以發現我們建構了如下的整個流程:這樣我們就可以愉快地發佈 Commands 和產生對應的 Events。

圖1

最後怎麼進行資料同步到讀取的 DataStore 裡呢?

在這裡 Commaned 函式庫推薦了 Commanded Ecto projections 來做 Event handler,或者也可以採用 Kafka 同步資訊,可以基於不同的應用場景選擇適合的 Event handler。

瞭解更多的 ArcBlock 系列講座

我們的講座資訊都將同步在:https://www.arcblock.io/zh/learning/

最後,如果您想要加入高品質、高效率的團隊,請加入 ArcBlock 吧!