l

2022年7月4日 星期一

事件溯源(7):樂觀鎖

June 30 22:15~24:00;July 01 00:00~00:59

▲圖1:不同Aggregate就不用鎖了

 

前言

假設有兩個使用者同時拿到同一個Tag並且將它改名然後儲存,系統要如何避免資料衝突?這是一個並行控制(Concurrency Control)的問題。在Event Sourcing系統中,一般採用樂觀鎖(Optimistic Locking)或稱為樂觀並行控制(Optimistic Concurrency Control)來解決這個問題。

今天介紹在領域驅動設計(Domain-Driven Design;DDD)與Event Sourcing的情境下,如何實作樂觀鎖定。

 

***

樂觀鎖

樂觀鎖的概念很簡單,以上述兩個使用者拿到同一個Tag並將其改名為例,首先Tag在資料庫中保存一個版本號碼,從資料庫讀出的Tag身上帶著這個版本號碼,然後在儲存的時候比對資料庫中的版本號與此時所存入的Tag版本號是否相同。若相同,則表示當初Tag從資料庫讀出之後,沒有其他人修改該Tag,因此可以直接寫入,寫入之後資料庫中該Tag版本號會被加1代表資料被更新過。如果寫入時Tag的版本號與資料庫中的版本號不同,則表示該筆Tag讀出後有人異動過它的資料,因此寫入失敗(不能用舊資料覆蓋掉比較新的資料),丟出樂觀鎖失敗的例外,使用者必須要重新載入新的資料,改修後再次儲存。

***

實作Aggregate與Repository以支援樂觀鎖

請參考圖2,ezKanban的AggregateRoot類別身上的version屬性(第16行)就是用來支援樂觀鎖,它的初始值為-1,表示尚未被寫入到資料庫中。

 

▲圖2:AggregateRoot支援樂觀鎖

 

當Aggregate被寫入資料庫以及從資料庫讀出的時候,Repository會負責設定它身上的version欄位。相關程式碼之前介紹Repository實作的時候已經看到,但當時Teddy並沒有解釋。現在再看一次,請參考圖3,首先看到GenericEventSourcingRepository的save方法,第52行透過eventSourcingStore儲存aggregateRootData之後,eventSourcingStore會更新aggregateRootData身上的version欄位。在第53行接著更新aggregate身上的version欄位。

接著看findById方法,第36行產生aggregate之後,第37行設定它的version欄位。


▲圖3:GenericEventSourcingRepository更新Aggregate的version欄位

 

在資料庫層面,EventStoreDB本身就支援樂觀鎖,回憶一下圖4的程式碼,第36~36行設定expectedRevision,如此一來EventStoreDBClient在寫入資料的時候就會啟動樂觀鎖定檢查。


▲圖4:EventStoreDB寫入時設定樂觀鎖定程式碼

 

至於以PostgreSQL所實做的Message DB也支援樂觀鎖定,但是如果把Message DB當成Outbox使用,則除了儲存領域事件以外,還需要儲存ORM資料,而ORM資料也有樂觀鎖設定的問題。在ezKanban中,把PostgreSQL當成Outbox使用的情況下,採用ORM的樂觀鎖機制,至於寫入領域事件則不做樂觀鎖檢查。

最後看一下ORM要如何設定樂觀鎖,請參考圖5。在TagData身上加上long version屬性(第28行),然後幫它貼上@Version annotation(第26行),如此便可啟動資料庫的樂觀鎖機制。

 

▲圖5:TagData在version術性加上@Version即可自動啟動樂觀鎖

 

***

 

測試

講了老半天怎麼知道樂觀鎖到底有沒有作用?寫個測試案例就知道了,請參考圖6。第106行、107行讀出同一個Tag,分別放在tagV1與tagV2變數。第108行修改tagV1的名稱,然後儲存tagV1。此時資料庫中該Tag的版本已經被加1,但是記憶體中tagV2的版本號碼還是舊的。因此第112行儲存tagV2便會丟出樂觀鎖失敗例外。這個測試案例分別注入Event Sourcing與Outbox repository,執行結果都通過,如圖7所示。

 


▲圖6:樂觀鎖測試案例

 

▲圖7:測試案例執行結果

 

***

下集預告

Event Sourcing的基本知識介紹得差不多了,下一集開始介紹CQRS,等CQRS介紹完畢後在回頭談Event Sourcing的進階議題,例如快照與事件版本異動。

***

友藏內心獨白:終於要進入CQRS。

沒有留言:

張貼留言