l

2022年7月9日 星期六

事件溯源(12):建立快照以加速聚合讀取

July 04 21:23~23:39

▲圖1:使用快照加速Aggregate讀取速度

 

前言

Event Sourcing系統在寫入端非常簡單且快速,但讀取因為需要從頭到尾逐一套用Aggregate instance所屬的所有領域事件以獲得最新狀態,經常讓人有一種「很慢」的感覺(錯覺?)。因此談到Event Sourcing除了套用CQRS將寫入與讀取模型分離以加快讀取速度以外,另一種常見加速的方式就是幫Aggregate的狀態建立快照(Snapshot)這一集就談這個議題。

 

***

快照原理

請參考圖1,Account Aggregate的原始資料儲存在Event Stream裡面,每次AccountRepository載入Account,必須從E1到EN傳給Account重新apply這些事件以計算出最新狀態。為了加速載入Account,幫Account的某個版本產生一筆快照,並將這個快照儲存至Snapshot Stream。下次AccountRepository再載入該Account時,會先到Snapshot Stream找出最新一筆快照,然後將快照的資料寫回Account,接著再從原本的Event Stream讀取快照之後所產生的新事件,然後將這些事件傳給Account再apply一次即可。

在圖1中,Snapshot Stream中有兩筆快照,第一筆 version=10,代表它是前10個事件的快照。第二筆快照是目前最新的快照,version=20,代表它是前20個事件的快照。當AccountRepository載入該Account時,它讀取Snapshot Stream最後一筆資料獲得最新的快照,因為快照的版本是20,所以AccountRepository接著從Event Stream的第21個位置開始讀取事件直到結束,然後把這些事件重新apply一次。在這個例子中,原本需要套用 N 個事件才可以獲得最新狀態,有了快照之後只需要套用 (N – 20) 個事件即可。

 

***

實作

實作快照的方法很多,一種常見的做法是讓Aggregate套用Memento設計模式產生快照,並自行從快照回復狀態。圖2是Memento設計模式類別圖,這個設計模式有三個角色:

  • Originator:需要保存快照資料的物件,以等一下要介紹的ezKanban例子而言,就是Tag Aggregate。
  • Memento:快照,在Memento設計模式裡面快照稱為Memento(備忘錄)
  • Caretaker:負則儲存快照的物件,ezKanban例子而言可以直接修改TagRepository程式碼讓它扮演Caretaker。如果想要遵守開放封閉原則不想修改原本已經可以動的TagRepository,則可以套用Decorator設計模式在Repository身上「外掛」快照的功能。

 

▲圖2:Memento設計模式類別圖

 

***

圖3是ezKanban中所定義的快照介面,它只有getSnapshot()與setSnapshot()兩個方法,前者用來產生快照,後者用來從快照回復狀態。


▲圖3:Memento介面

 

圖4為Tag Aggregate實作Memento介面的程式碼,第14行的TagSnapshot record就是快照本人,它代表Tag的目前狀態。第25行getSnapshot方法直接回傳一個新的TagSnapshot,第30行的setSnapshot方法則是將傳入的TagSnapshot身上的值寫入Tag,等於回復Tag的狀態。第18行是一個static factory method,可以直接從TagSnapsht得到一個Tag物件。

 

▲圖4:Tag Aggregate實作Memento介面

 

圖5為支援快照版本的TagRepository的save方法,第72行儲存原本Tag的領域事件,第74行~第84行則是判斷是否需要儲存快照。這個版本的TagRepository會依據使用者所設定的snapshotIncrement數值決定多少筆領域事件紀錄一次快照,如果snapshotIncrement等於100,則超過100筆會記錄一次快照。

 

▲圖5:TagRepository的save方法(支援快照版本)

 

圖6為支援快照版本的TagRepository的findById方法,第46行從Snapshot Stream載入舊的Snapshot領域事件,如果Snapshot不存在則第49行直接用原有的方式載入Tag;如果存在,第53行由Snapshot產生Tag,然後再載入Snapshot版本號加1的Tag原有領域事件(第54~55行),然後逐一apply它們(第56行)。

 

▲圖6:TagRepository的findById方法(支援快照版本)

 

以上產生與載入快照的方式,除了可以用在Aggregate身上,也可以用在Event Sourced Read Model上面,請參考上一集<事件溯源(11):撰寫JavaScript在EventStoreDB中產生自訂投影>所提到的加速EventStoreDB採用使用者自訂Projection做為Read Model的做法。

 

***

你真的需要快照嗎?

如果採用上述方式實作快照,因為套用Memento設計模式,所以Aggregate Root需要實作Memento介面。也就是說,從Clean Architecture的角度來看,不僅僅是位於Adapter層的Repository實作需要修改,連Entity層的Aggregate也要改。這雖然不是什麼大不了的修改,但畢竟讓系統變得更複雜。除非必要,否則不需要建立快照。

什麼叫做必要?這要從Aggregate的生命周期長短來判斷。以ezKanban為例,Card Aggregate代表看板上面的一項工作,它從出生到死亡(工作完成並歸檔),身上的領域事件可能大不了幾十個。在這種情況下,幫Card建立快照其實是不需要的。但是,如果是銀行的Account物件,代表使用者的交易紀錄。通常銀行客戶可能存在好幾年甚至數十年,累積的交易紀錄非常可觀。此種情況下,如果不透過快照來加速,可能會有效能的問題。

但是Event Sourcing系統中也不是只有快照和CQRS這兩種方式可以增加讀取速度,還有一種「年度結算(固定時間結算)」的做法和快照類似。以銀行為例,每一年度開始可能會把去年整年每一筆Account交易紀錄「濃縮(壓縮)」成一筆新的領域事件,並且從原本的event stream中將舊有的事件移至另一個event stream(或是每一年度產生一個新的event stream亦可)。

Teddy曾經在某本書(還是文章,忘了來源)看到一種說法:「領域事件少於一萬都不需要做快照。」這個數據鄉民們可以參考看看。

***

 

下集預告

下一集談另一個比較進階但卻很重要的議題:Event Versioning(事件版本異動)

***

友藏內心獨白:Memento設計模式終於派上用場了。

沒有留言:

張貼留言