天天看點

ENode架構Conference案例分析系列之 - 複雜情況的讀庫更新設計

conference案例,是一個關于線上建立會議(類似qcon這種全球開發者大會)、線上管理會議位置資訊、線上預訂某個會議的位置的,這樣一個系統。具體可以看微軟的這個項目的首頁:http://cqrsjourney.github.io。

然後我們設計了一個conference聚合根,對應領域中的會議這個領域概念。conference聚合根下面,有一些位置資訊seattype。一個會議聚合根下面可以添加不同類型的位置,每種類型的位置可以指定數量以及價格。是以,conference是聚合根,conference本身有一些我們所關心的基本屬性,同時它内部聚合了一些seattype子實體。每個seattype包含了位置的價格、數量這兩個資訊。

然後,在ui層面,我們會有如下界面邊界管理一個會議的所有位置資訊。

ENode架構Conference案例分析系列之 - 複雜情況的讀庫更新設計

上圖列出了某個會議的兩類位置,quota表示位置的配額數量;當我們要修改某種位置時,可以點選連結,然後出現如下圖所示:

ENode架構Conference案例分析系列之 - 複雜情況的讀庫更新設計

出現四個編輯框,我們可以修改任何一個框。修改完後點選儲存,我們就能更新某個類型的位置資訊了。然後,我們在domain裡,設計了兩個domain event;分别表示位置基本資訊改變和位置配額數量的改變。

為什麼要獨立出數量改變的domain event呢?因為當使用者在前台下單訂購位置時,這個數量也會變化。也就是位置數量可能會單獨變化。是以,我們考慮單獨為位置數量的變化定義一個domain event。

然後,我們目前的代碼是,當點選儲存時,首先更新會更新位置的基本資訊,然後判斷數量是否有變化,如果沒變化,則隻産生位置基本資訊變化的domain event;如果有變化,則同時産生位置數量改變的domain event。conference聚合根相關方法的具體實作如下:

ENode架構Conference案例分析系列之 - 複雜情況的讀庫更新設計

上面的代碼的大緻意思是,先從聚合内找出需要修改的位置類型,如果不存在就抛異常;如果存在,則先産生位置基本資訊的改變事件;然後判斷數量是否有變化,如果有變化,則繼續判斷目前輸入的數量是否太小,如果太小也是不允許的。

比如,假如使用者錄入的數量是10,但是目前這種類型的位置已經有11個被預定了,那就不能改為10,而是必須至少為11。最後,如果一切都合法,就産生一個seattypequantitychanged的事件,表示某個類型的位置的數量發生了變化,同時在事件中帶上可預定的剩餘位置的數量。

然後讀庫我們就根據上面這兩個事件來更新。

現在的問題是,假如兩個事件都發生了,那讀庫要怎麼原子更新(在一個事務裡更新)?我們的一個event handler隻能處理一個event;也就是說,我們會有兩個event handler,分别處理對應的事件。由于domain aggregate是一次性原子的方式同時産生兩個domain event。是以,我們要確定兩個event handler要麼都更新成功,要麼都不更新成功,這個問題之前沒考慮到過,下面我們來想想辦法。

想辦法把這兩個event handler包裝在一個事務裡,但這要求架構支援這樣的跨多個event handler的事務機制;對架構要求的的改造有點大,複雜度高,不太可行。因為架構要考慮的問題是要更通用的,比如,一旦引入事務,也許還會引入分布式事務等問題。而且這種做法,性能也不高,違反enode一開始就是為高并發設計的初衷。

要求領域裡不要設計兩個domain event了,就用一個domain event解決;這個event包含所有資訊的修改,包括數量的修改。這個辦法可行,但要求模型做出妥協和讓步了。假如有一天我們遇到模型必須要産生多個事件的情況,那怎麼辦呢?是以,這個思路還是在逃避問題。

不采用事務,而是采用樂觀鎖+順序控制+幂等支援的方式解決問題。思路是,架構按照順序調用這兩個event handler,調用的順序和這兩個事件的順序一緻;兩個event handler允許不在一個事務裡。

這樣的問題是,假如第一個事件處理成功了,然後此時機器斷電了,第二個事件沒被處理,怎麼辦?那就是要做到,當下一次機器重新開機後,第二個事件能被處理。然後,因為整個架構是分布式的,是以第一個事件也是有可能被重複處理的,架構在調用event handler時,為了性能方面的考慮,隻會盡量保證同一個event不會被同一個event handler重複處理,不會絕對保證;但是架構有提供機制,讓開發人員在event handler内部通過依賴版本号的方式來解決重複處理的問題。是以,總結一下,我們需要處理的問題有以下3個:

需要保證任何event handler内部自己能做到絕對的幂等,架構提供支援;

需要保證任何一個event至少被處理一次,即便是在任何時候斷電的情況下;

需要保證同一個事件流裡的事件,處理的順序也要按照事件流的順序處理;

為了做到上面這3點,我對enode做了一個完善,就是為事件引入了一個子版本号的概念。

就是當聚合根每次做出修改後,不管産生多少個domain event,這些domain event都是在一個event stream裡;每個event stream都有一個版本号,然後每個domain event的主版本号就是其所在的event stream的版本号。比如某個聚合根某次變化産生了2個domain event,它們被保證在一個event stream裡,然後假如這個event stream的版本号為10,那每個domain event的主版本号也是10;這點enode架構可以做保證。那event stream的版本号哪裡來的呢?就是從聚合根上得來,因為每個聚合根都維護了目前自己的版本号是什麼,用version表示,那它下一次産生的event stream的版本号就是version+1。

上面解釋了什麼是事件的主版本号。下面我們在說一下什麼是事件的子版本号。子版本号比較簡單,就是假如一個event stream裡包含2個事件,那第一個事件的子版本号是1,第二個則是2;是以,其實子版本号就是事件在事件流裡的順序号。

然後,有了事件的主版本号和子版本号的概念。我們就可以做到上面的3點要求了。其中的第2點,equeue會做到確定任何一個消息至少被處理一次,這裡不做展開了。第1、3點,我們通過下面的代碼結合分析讨論。

ENode架構Conference案例分析系列之 - 複雜情況的讀庫更新設計
ENode架構Conference案例分析系列之 - 複雜情況的讀庫更新設計

為了代碼效果好一點,我直接通過截圖的方式了,部落格園以後官方提供一套這樣的代碼模闆吧,呵呵。@蟋蟀,上次你跟我說的那個模闆,我後來忘記使用了:)

上面的代碼中,每個event handler内部有一個事務,為什麼還需要事務?因為我們現在更新的是聚合根,子實體(位置資訊)是聚合根的一部分;是以讀庫更新時,自然也要更新聚合根本身的。隻不過這裡隻需要更新聚合根的版本号即可。

第一個event handler,我們先啟動一個事務,然後先更新聚合根的主版本号,以及次版本号;假如資料庫裡conference記錄的目前的主版本号是10,次版本号是1,那這個evnt.version就是11,evnt.sequence是1,sequence就是次版本号。然後通過第一條update sql我們就能更新聚合根的主版本号以及次版本号了。由于單條update sql是原子事務(無并發問題)的,是以我們隻要判斷更新的影響行數是否為1。如果是1,則說明更新成功,那就可以更新位置那條記錄了。然後,由于這兩條更新語句在一個事務裡,是以要麼全部完成,要麼什麼都不做,不會有做了一半的情況。

第二個event handler,同樣,我們也是先啟動一個事務。然後差別是,因為我們知道seattypequantitychanged事件和seattypeupdate事件總是在一個事件流裡發生的,且它總是位于第二個順序。是以,當這個event handler被執行時,聚合根的主版本号一定已經是11了,且子版本号是1。那麼,我們在第二個event handler中,對聚合根,隻需要更新子版本号為2即可。就是第一個update語句。然後同樣判斷影響行數是否為1。如果是,則更新位置的數量以及可用數量;如果不是1,則什麼都不做。

有一個問題,什麼時候會出現不是1呢?就是在這個event handler被重複執行的時候。這種情況,我們忽略即可。因為我們就是為了要做到update的幂等處理。

到這裡基本差不多了。但是還需要說明一個大前提。就是上面這個大家可以看到,第一個event handler裡,更新聚合根的主版本号時,where條件裡會判斷聚合根記錄的目前版本号是evnt.version - 1;這個就是為了保證,讀庫更新時,總是按照domain event的發生順序依次更新的,不能跳過更新,也不能亂序。否則讀庫的最終資料就不一緻了。是以,event handler内部要做這樣的判斷,確定絕對不會發生這樣的事情。但光event handler内部判斷還不夠。enode架構也要保證event stream消息的處理順序也是這樣依次按照順序的,否則event handler裡聚合根更新的影響行數也許永遠都不能為1了。

enode已經意識到這個問題,是以已經幫我們做了這樣的保證!

上面的最後一個方案,我覺得是比較通用的解決方案。架構不需要做支援跨event handler的事務,改動比較小。同時還能保證讀庫更新的性能,另外,在斷電的時候,也能保證事件被處理。

總之,一切的一切都是為了高性能、為了保證最終一緻性。又花了一篇文章分享了一點小小的設計,呵呵。