2012-02-29 40 views
54

讓我們以一個簡單的「註冊帳號」的例子,這裏是流量:CQRS事件採購:驗證用戶名的唯一性

  • 用戶訪問網站
  • 點擊「註冊」按鈕,填寫表格,點擊「保存」按鈕
  • MVC控制器:驗證用戶名的唯一性由ReadModel
  • RegisterCommand閱讀:再次驗證用戶名的唯一性(這裏是問題)

當然,我們可以通過讀取MVC控制器中的ReadModel來驗證UserName的唯一性,以提高性能和用戶體驗。但是,我們仍然需要在RegisterCommand中再次驗證唯一性,顯然,我們不應該在Commands中訪問ReadModel。

如果我們不使用事件採購,我們可以查詢領域模型,所以這沒有問題。但是如果我們使用事件採購,我們無法查詢域模型,所以我們如何驗證RegisterCommand中的用戶名唯一性?

說明:用戶類具有Id屬性,UserName不是User類的關鍵屬性。使用事件採購時,我們只能通過Id獲取域對象。

BTW:在需求,如果輸入的用戶名已被使用,該網站應顯示錯誤消息「對不起,該用戶名XXX不可用」的訪問者。顯示一條消息,例如說,「我們正在創建您的帳戶,請等待,我們會在稍後通過電子郵件將註冊結果發送給您」,這是不能接受的。

任何想法?非常感謝!

[更新]

更復雜的例子:

要求:

下訂單時,系統應檢查客戶的訂貨歷史,如果他是一個有價值的客戶(如果客戶在過去一年每月至少訂購10份訂單,​​他很有價值),我們將訂單打九折。

實現:

我們創造PlaceOrderCommand,並在命令中,我們需要查詢訂貨歷史,看看如果客戶是有價值的。但我們怎麼做到這一點?我們不應該在命令中使用ReadModel!作爲Mikael said,我們可以在帳戶註冊示例中使用補償命令,但是如果我們也在此排序示例中使用補償命令,則它會太複雜,並且代碼可能太難以維護。

回答

34

如果您在發送命令之前使用讀取模型驗證用戶名,我們正在討論一個幾百毫秒的競態條件窗口,其中可能會發生真正的競爭條件,這在我的系統中未得到處理。與處理它的成本相比,它不太可能發生。

不過,如果你覺得你必須處理它由於某種原因,或者如果你只是覺得你想知道如何掌握這種情況下,這裏是一個辦法:

你不應該訪問從讀模式命令處理程序或使用事件源時的域。但是,您可以執行的操作是使用一個域服務,該服務將監聽您再次訪問讀取模型的UserRegistered事件,並檢查用戶名是否仍然不重複。當然,您需要在這裏使用UserGuid,並且您的讀取模型可能已用您剛剛創建的用戶更新過。如果發現重複,則可以發送補償命令,例如更改用戶名並通知用戶該用戶名已被佔用。

這是解決問題的方法之一。

你或許可以看到,這是不可能做到這一點的同步請求 - 響應方式。爲了解決這個問題,我們使用SignalR來更新UI,只要有一些我們想要推送給客戶端的東西(如果它們仍然連接,那就是)。我們所做的是讓Web客戶端訂閱包含對客戶立即查看有用的信息的事件。

更新

對於更復雜的情況:

我想說的順序放置不是很複雜的,因爲你可以使用讀取模型,找出客戶端是否有價值在發送命令之前。事實上,當你加載訂單時,你可能會詢問,因爲你可能想向客戶展示在下訂單之前他們會得到10%的折扣。只需在PlaceOrderCommand上添加折扣,這也許是折扣的原因,這樣您就可以追蹤爲什麼會降低利潤。

但話又說回來,如果你真的需要計算折扣後的順序是出於某種原因的地方,再次使用會聽OrderPlacedEvent和「補償」域名服務在這種情況下,命令可能會是一個DiscountOrderCommand或一些東西。該命令會影響Order Aggregate根,並且信息可能會傳播到您的讀取模型。

對於重複的用戶名的情況:

您可以發送ChangeUsernameCommand爲來自域服務的補償命令。甚至更具體的東西,這將描述用戶名更改的原因,這也可能導致創建Web客戶端可以訂閱的事件,以便您可以讓用戶看到用戶名是重複的。

在域名服務方面,我要說的是,你還必須使用其他手段來通知用戶,例如發送一樣,因爲你可以不知道,如果用戶仍處於連接狀態可能是有用的電子郵件的可能性。也許這個通知功能可能是由Web客戶端訂閱的相同事件啓動的。

當談到SignalR,我使用SignalR樞紐的方便用戶連接時加載一定的形式。我使用SignalR Group功能,它允許我創建一個名爲我在命令中發送的Guid值的組。這可能是你的情況userGuid。然後我有Eventhandler訂閱可能對客戶端有用的事件,並且當事件到達時,我可以調用SignalR Group中所有客戶端上的javascript函數(在這種情況下,只有一個客戶端在您的賬戶中創建重複的用戶名案件)。我知道這聽起來很複雜,但事實並非如此。我把它全部在一個下午建立起來。 SignalR Github頁面上有很多文檔和例子。

+0

當我發現用戶名重複時,我應該在補償命令中做些什麼?發佈SignalR事件以通知客戶端用戶名不可用? (我沒有使用過SignalR,我想可能會有某種「事件?」) – 2012-02-29 09:21:32

+0

很好的答案,謝謝!但是我對「域名服務」感到困惑,你是不是指「事件處理程序」?我認爲它與DDD中的「域服務」不一樣嗎? – 2012-02-29 13:00:14

+1

我認爲我們稱之爲DDD中的應用服務,但我可能弄錯了,而域服務在DDDD/CQRS社區中是一個爭論的話題。除了你可能不需要狀態機或狀態機之外,它們與他們所說的Saga類似,你只需要一些能夠反應和反饋事件,執行數據查找和調度命令的東西,我稱之爲域服務。訂閱事件和發送命令,這在聚合根節點之間進行通信時非常有用 – 2012-02-29 14:48:35

18

我想你還沒有擁有的心態轉變爲eventual consistency和採購活動的性質。我有同樣的問題。具體而言,我拒絕接受您應該信任來自客戶的命令,使用您的示例,如果沒有域驗證折扣應該繼續,請說「以10%的折扣下訂單」。有一件事對我來說真的很重要,它是something that Udi himself said to me(查看接受的答案的評論)。

基本上,我才明白,沒有理由不信任客戶端;讀取方面的所有內容都是從域模型生成的,所以沒有理由不接受這些命令。無論在什麼方面,客戶都有資格獲得折扣,這一點已經被域名放在那裏。

順便說一下,在要求中,如果輸入的用戶名已被使用,網站應該向訪問者顯示錯誤信息「對不起,用戶名XXX不可用」。顯示一條消息,例如說,「我們正在創建您的帳戶,請等待,我們會在稍後通過電子郵件將註冊結果發送給您」,這是不能接受的。

如果你打算採用事件採購&最終一致性,則需要接受有時它會不會有可能提交命令後立即顯示錯誤消息。使用唯一的用戶名示例,發生這種情況的可能性非常小(因爲您在發送命令之前檢查了讀取方)它不值得擔心太多,但是需要爲此方案發送後續通知,或者可能會詢問他們在下次登錄時使用不同的用戶名。關於這些情況的好處是,它讓你考慮到商業價值&真正重要的是什麼。

更新:2015年10月

只是想補充一點,其實不然,在面向公衆的網站而言 - 這表明郵件已經被佔用實際上是對安全性的最佳實踐。相反,註冊似乎已成功通知用戶已發送驗證電子郵件,但在用戶名存在的情況下,電子郵件應通知他們並提示他們登錄或重置密碼。雖然這隻適用於使用電子郵件地址作爲用戶名,我認爲這是明智的。

+3

優秀的輸入。這是在系統可以之前必須改變的想法(我並不打算在那裏聽起來像尤達)。 – 2012-02-29 21:06:12

+4

+1在這裏真正*迂腐...... ES和EC是兩個完全不同的東西,使用一個不應該暗示使用其他(儘管在大多數情況下它是非常有意義的)。使用ES沒有最終一致的模型是完全有效的,反之亦然。 – James 2014-05-01 14:33:20

+0

「基本上我意識到沒有理由不相信客戶」 - 是的,我認爲這是一個公平的評論。但是,如何處理可能產生命令的外部訪問呢?顯然,我們不希望允許帶有自動應用折扣的PlaceOrderCommand;折扣的應用是領域邏輯,而不是我們可以「信任」某人告訴我們應用的東西。 – 2014-05-17 07:35:28

11

沒有什麼錯與創建一些立即讀一致性模型(例如不通過分佈式網絡),它們會在相同的事務中的命令進行更新。

看了模型是最終通過分佈式網絡一致的幫助重讀取系統讀取模型的支持比例。但沒有什麼可說的,你不可能有一個特定領域的讀取模型,它們立即一致。

立即一致的讀取模型僅用於在發出命令(真正的命令服務)之前檢查和接收數據,您不應該使用它直接顯示讀取數據給用戶(即從GET網絡請求或類似)。爲此,最終使用consitent,可擴展的讀取模型。

4

我想對於這樣的情況下,我們可以使用類似「諮詢鎖用過期」的機制。

樣品執行:

  • 檢查用戶名在最終一致的讀取模式的存在與否
  • 如果不是存在;通過使用像keyvalue存儲或緩存一樣的redis-couchbase;嘗試推送用戶名作爲關鍵字段有一段時間。
  • 如果成功;然後引發userRegisteredEvent。
  • 如果在讀取模型或緩存存儲中存在任一用戶名,請通知訪問者該用戶名已被佔用。

即使您可以使用sql數據庫,將用戶名作爲某個鎖定表的主鍵;然後計劃的作業可以處理到期。

3

像許多其他實施基於事件源的系統一樣,我們遇到了唯一性問題。

起初,我是一個支持者,讓客戶端在發送命令之前訪問查詢端,以確定用戶名是否唯一。但是後來我發現有一個對獨特性沒有任何驗證的後端是一個壞主意。爲什麼在可能發佈會破壞系統的命令時執行任何操作?後端應驗證所有的輸入,否則你打開不一致的數據。

我們所做的是在命令端創建index。例如,在一個簡單的用戶名需要唯一的情況下,只需創建一個帶有用戶名字段的UserIndex。現在命令端可以檢查用戶名是否已經在系統中。命令執行完畢後,將新的用戶名存儲在索引中是安全的。

類似的東西也可以用於訂單折扣問題。

好處是您的命令後端可以正確驗證所有輸入,因此不會存儲不一致的數據。

一個缺點可能是您需要對每個唯一性約束進行額外的查詢,並且執行額外的複雜性。

0

你有沒有考慮過使用「工作」緩存作爲RSVP的排序?這很難解釋,因爲它在一個循環中有效,但基本上,當一個新的用戶名被「聲稱」(即,該命令是爲了創建它而發出的)時,您將用戶名放入緩存並且過期很短足夠長的時間以解釋通過隊列的另一個請求並將其非規格化到讀取模型中)。如果它是一個服務實例,那麼在內存中可能會工作,否則集中Redis或其他東西。

然後,當下一個用戶填寫表單(假設有一個前端)時,您可以異步檢查讀取的模型是否有用戶名的可用性,並提醒用戶是否已經使用。提交命令時,您檢查緩存(不是讀取模型),以便在接受命令之前驗證請求(在返回202之前);如果名稱在緩存中,請不要接受該命令,如果不是,則將其添加到緩存;如果添加它失敗(重複鍵,因爲其他進程擊敗了它),然後假定名稱被採取 - 然後適當地響應客戶端。在這兩件事之間,我認爲沒有太多的碰撞機會。

如果沒有前端,那麼您可以跳過異步查找,或者至少讓您的API提供端點來查找它。您真的不應允許客戶端直接與命令模型直接對話,並將API放在它的前面,這樣您就可以讓API充當命令和讀取主機之間的中介。