CQRS解構
本文討論的是如何使用CQRS實現API設計。
概述
下面是名為Command / Query Responsibility Segregation(CQRS)的設計模式:
返回資料做出改變 查詢:heavy_check_mark::x: 命令:x::heavy_check_mark:
查詢和命令是兩種分離的API。
為何使用這種模式?我喜歡它有幾個原因。作為API的消費者,我永遠不必擔心使用API出現異常了;相反,我能確切地知道哪些API呼叫會專門應付對系統的更改請求,沒有含糊之處。這使得API 易於推理。
我曾經嘗試建立一個統一的介面來完成兩者,但是隨著時間推移,出現“服務於兩個主人“”的典型問題,單一介面變得更加混亂。
時間長了會發生:“我們不使用這個欄位,為什麼我們要更新它?。” 迴應:“我不知道,但繼續這樣做或會出問題。”
:package:
因此,CQRS的核心是一個關注點分離的特定應用,即良好的組織實踐。現在我們已經對模式進行了介紹,我將介紹一些實現細節和經驗教訓。
訊息
我認為每個查詢或命令都是一條訊息。這意味著任何客戶端系統都可以將這些表達為沒有方法的普通資料(類或結構)。然後以線形格式(如JSON或CapnProto或w / e)輕鬆傳輸它們。
每條訊息都有一個名稱 - 通常只是類/結構名稱 - 它在API中唯一標識它。如SearchCustomers(查詢)或DeactivateCourse(命令)。名稱用於標識請求的操作,然後將其與訊息解析器和處理函式進行匹配。安全授權由此很簡單,只要保留一個使用者名稱單,允許這些使用者可傳送哪些訊息名稱;然後在處理任何使用者的訊息之前檢查該列表。
運營
命令和查詢應該如何工作似乎很明顯。但是我發現了一些細微差別。
查詢:
查詢是很簡單的:
1. API偵聽 /query/[Query Name]
2. 驗證使用者是否具有[Query Name]許可權
3. 反序列化查詢訊息
4. 將查詢訊息傳遞給其處理函式,該函式將:
5. 驗證查詢訊息
6. 從資料庫載入和轉換資料
7. 序列化並返回資料
命令:
命令的目的是在系統上執行一些業務操作。在實踐中,我們注意到命令是否需要更改一個或多個實體。出於架構原因,如何處理多個實體更改非常重要。
:pushpin: 在這種情況下,實體意味著某個邏輯單元。在高度規範化的表中,實體可能包含父物件和一對多關係的任何子物件。在DDD術語中,你可以將其稱為聚合。在事件溯源中,這是一個事件流。
可擴充套件性
你可以在單個事務中執行多實體更改,以實現全有或全無的事務語義。這種方法很適合,但它限制了可伸縮性。要參與事務,所有受影響的實體必須位於同一資料庫節點上。如果它們位於不同的節點上,則發生分散式事務(如果資料庫支援)。隨著負載的增加,分散式事務將逐漸變慢。跨實體事務是企業內部業務應用程式的有效方法,但對於公開的網際網路服務,也許不是。
像網際網路這種更加大規模級別應用,我們的方法是僅使用單實體命令進行更改。當用例需要更改多個實體時,請使用元命令(自身不做任何改變),而不是編排並執行單實體命令。我將單實體命令稱為“基本命令”,將多實體命令稱為“工作流程”。當工作流呼叫基本命令時,這些命令可能會失敗,工作流程會以業務用例的方式處理故障。這可能意味著需要忽略失敗並作為業務錯誤/警告返回給客戶,或採取補償回退措施。
客戶端工作流
你可以在客戶端實現工作流 - 讓UI編排所有必要的基本命令。但是,我不在客戶端實現工作流,而在API端實現工作流,主要理由是清晰(特別是安全性)。
我用一個真例項子來說明。我們有培訓師的角色,這個角色不能建立課程,但是,他們可以記錄他們提供給員工的培訓。記錄培訓時可能需要建立一個有限選項的新課程。將記錄培訓作為API工作流執行,可以將其表示為單個細化許可權:“培訓師可以記錄培訓,但不能建立課程。” 在許可權UI上選中一個選項,而不選中另一個選項。
如果採取客戶端工作流,執行上述相同的操作,我們看看會怎樣?我們需要新增一個基本命令:建立培訓師課程,然後管理員使用者必須被告知:“要給某人錄製培訓的許可權,你必須檢查Create Trainer Course並且Permission X和Permission Y。” 那麼這就給終端使用者造成的負擔,需要他做這些流程。在這裡,我們還可以建立一個偽命令,僅用於許可權目的,對映到所需的基本命令。這就將負擔又轉移給開發人員。我不喜歡這些結果中的任何一個,所以我更喜歡API端工作流。
指導原則
在實施CQRS API時,人們會提出一些非常常見的問題。
1. 返回錯誤與返回資料不同。
一個流行的誤解是命令應該什麼都不返回。實際是命令應該返回一些東西。它們返回有關操作本身的元資訊(無論是成功還是失敗以及為什麼。這與返回業務資料非常不同,後者是查詢的工作。
2. 命令可以在不進行更改的情況下成功。
命令可以進行0次或更多次更改。換句話說,“進行更改”是命令的目的,而不是所需的結果。因此,對於成功執行的命令完全有效,但不會導致任何更改。
在執行命令之前和之後比較實體,如果它們完全相同,那麼就是進行0更改併成功返回。
3. 命令處理程式碼可以呼叫查詢。
這似乎違反了CQRS原則。命令的內部除了“進行更改”之外,它沒有任何意思。
因此,可以在命令中執行查詢,才能獲取命令所需的一些資訊,但是要小心一點。
4. 自動遞增ID不應是主鍵ID。
常常需要返回自動生成的ID,因為自動增量ID非常方便,但是也有問題:重複。
場景:使用者填寫表單用來建立新實體並點選提交。請求超時。
自動增量冒險:如果自動增量欄位是您唯一的ID,則你的應用無法知道請求是否成功。對這種情況的補救措施通常取決於使用者的意識和參與。
如果使用者再次點選提交(非常可能),但是先前的請求如果確實建立了實體,儘管超時,那麼現在有兩個具有不同ID的相同實體。要正確清理,使用者現在應該搜尋重複項並刪除冗餘實體(極不可能)。
或者,在超時之後,使用者可以搜尋他們可能建立的實體。如果他們找不到,請再回來填寫表單。根據我的經驗,這種情況不太可能。如果培訓使用者習慣於這樣思考,則可能會發生這種情況。
還可以新增重複檢查的外部系統,例如保持對已檢視操作及其結果的記憶。但有更好的方法......
預先生成的ID(冒險):
在使用者甚至開始鍵入任何內容之前,在載入表單時就生成(或從伺服器請求)ID。
在使用者被告知請求超時後,她再次點選提交。介面就會使用相同的預生成ID傳送與之前相同的完全相同的請求。如果資料庫沒有重複會成功,否則API響應:“此實體已存在。” 如果UI可以識別出這個特定錯誤,它可以假裝它正常成功。這種冒險帶來了更好的使用者體驗,沒有重複的機會。
我們的策略:我們傾向於使用UUID進行所有標識。它們很容易在許多平臺上生成。他們無視趨勢分析。我們的大多數建立表單都必須執行查詢(例如,獲取下拉列表資料),因此我們會在查詢結果中包含一個新的UUID。
結論
命令是變動的守門人。查詢則是知識庫,這是CQRS。我發現這種模式使我走向正確的方向。它也是一種多功能的模式。它不關心你的部署的是單體還是微服務。甚至可以將命令和查詢拆分為各自獨立的服務,實現讀寫負載分離。
但請記住,這只是一個大系統中的一個部分,而不是適合每個系統的通用工具。CQRS模式在後端系統的邊界內能很好地工作,與客戶端應用程式連線。與任何模式一樣,只有在適當的情況下應用它時才有用。