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

作者 周蕾(ArcBlock 後端工程師)
最近幾年,在 DDD 的領域,我們經常會看到 CQRS 架構的概念,CQRS 是查詢職責分離模式(Command Query Responsibility Segregation)的縮寫。正好這些日子 ArcBlock 的後端服務有考慮使用 CQRS 的架構,所以今天和大家一起分享一下最近的研讀收穫。今天文章會從 Event Sourcing 出發介紹 CQRS,以及透過 Commanded(Elixir 的函式庫),一起看一看如何遵循 ES/CQRS 的概念開發應用程式。
什麼是 Event Sourcing(事件溯源)?
Event Sourcing 本質來說是保存了發生事件的本身,而不是當前事物的狀態。在 Event Sourcing 的概念裡,Event 作為既定發生之後的事情,也是最小的單位。 例如:

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

我們會得到這樣的總結:
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)分離開來,這也意味著在查詢和更新過程中使用的資料模型也是不一樣的。這樣讀取和寫入邏輯就隔離開來了。

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
]
end2. Events
Events 是由 Command 產生,最終導致狀態改變。最終會在 eventstore 裡面序列化儲存,可以用於日後想要恢復狀態。命名方式相比於 Command 來說發生了變化,CoinMined,以過去式表達一種過去發生的事實。
defmodule CoinMined do
defstruct [
:account_id,
:nonce
]
end3. 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。

最後怎麼進行資料同步到讀取的 DataStore 裡呢?
在這裡 Commaned 函式庫推薦了 Commanded Ecto projections 來做 Event handler,或者也可以採用 Kafka 同步資訊,可以基於不同的應用場景選擇適合的 Event handler。
瞭解更多的 ArcBlock 系列講座
我們的講座資訊都將同步在:https://www.arcblock.io/zh/learning/
最後,如果您想要加入高品質、高效率的團隊,請加入 ArcBlock 吧!