認知篇:CQRS架構模式的本質

京東雲開發者發表於2023-01-30

作者:京東科技 倪新明

CQRS只是一種非常簡單的模式(pattern),CQRS本身並不是一種架構風格,和最終一致性/訊息/讀寫分離/事件溯源/DDD等沒有必然的聯絡,它最大優勢是給我們帶來更多的架構屬性選擇

1 CQRS 本質

1.1 CQS:命令和查詢分離

命令和查詢分離,Command and Query Segregation,其核心思想是在任何一個物件的方法可以劃分為兩類

•查詢:獲取資料,返回查詢資料,但不改變資料狀態
•命令:改變資料狀態,不返回任何資料

基於CQS的思想,任何一個方法都可以拆分為命令和查詢兩部分:

private int origin = 0;
private int add(int value)
{
    origin += value;
    return origin;
}

上述方法既改變了資料,又返回了資料狀態,如果按照CQS的思想,則該方法可以拆成Command和Query兩部分,如下:

private void add(int value)
{
    origin += value;
}
private int queryValue()
{
    return origin;
}

是否嚴格遵循上述約定存在爭議,對於命令側是否返回資料實際業務訴求中並不一定能夠完全統一。比如:

•"出棧" 操作同時改變棧狀態和返回資料
•某些業務場景下可能會有返回業務主鍵的訴求,比如下單操作返回訂單號

1.2 CQRS:命令和查詢職責分離

Command and Query Responsibility Segregation,即命令查詢職責分離,由Greg Young提出 。CQRS在CQS基礎之上,將分離的級別從程式碼方法級別擴充套件到物件級別。CQRS 模式的應用非常簡單,如下圖所示

 

 

假設我們的服務為 OrderService,在非CQRS模式下同時包含了查詢和更新服務介面:

public class OrderService {
   //  根據id查詢訂單
    Order getOrder(OrderId)
    // 查詢已支付訂單
    List<Order> getPayedOrders()
    // 下單
    void placeOrder(Order)
    // 取消訂單
    void cancelOrder(OrderId) 
}

應用CQRS模式之後的OrderService被拆分成了兩個介面,分別承擔查詢和寫職責:

/**
命令側服務
*/
public class OrderService {
    void placeOrder(PlaceOrderCommand command)
    void cancelOrder(CancelOrderCommand command)
}
/**
 查詢服務
*/
public class OrderQueryService{
    Order GetOrder(OrderId)
    List<Order> getPayedOrders()
}

以上這種簡單的分離就是CQRS模式的全部了,是不是非常簡單?確實,單純的看,CQRS的確就是這麼簡單。

CQRS最大優勢就是基於這種職責分離能帶給我們更多的架構屬性選擇

•“查詢” 和 “命令” 兩側進行獨立部署以獲取更好的伸縮性
•“查詢” 和 “命令” 兩側獨立架構設計
•“查詢” 和 “命令”兩側進行獨立資料模型設計

基於CQRS,我們可以衍生出更多的架構屬性,結合實際的業務場景,進行差異化的架構設計。

團隊引入CQRS模式之後,往往不僅僅是簡單的在類的職責層面對讀寫進行分離,一般會採用更為複雜的應用架構風格,如下是典型的CQRS架構風格:

 

 

•命令側:命令側引入命令匯流排以支援對不同命令的靈活路由;突出領域模型的應用
•查詢側:引入查詢匯流排對查詢請求進行路由;請求鏈路一般直接連線到儲存層,實現不同的定製化查詢需求

2 CQRS迷思

2.1 資料模型是否要分離

CQRS強調命令和查詢的職責分離,但在底層的資料模型層面,CQRS並沒有進行強制限定,即採用CQRS模式並沒有要求必須要進行資料模型的分離。是否要進行模型分離開發人員需要具體情況具體分析。

•分離模型:查詢側和寫側模型不互相干擾,各自在應用層的實現複雜度比較低。但由於模型的分離,命令側和查詢側的資料一致性需要納入考慮範圍
•不分離:不需要考慮資料一致性問題,但由於查詢側和寫側對模型的訴求可能不一致,模型的設計往往需要折衷考慮。

2.2 CQRS 和 訊息模式

CQRS和訊息模式沒有必然聯絡,落地CQRS 並不一定需要使用訊息模式

 

 

如果我們採用了CQRS模式,但是命令和查詢兩側底層所依賴的資料模型並未分離,而是基於共享的資料儲存和資料模型,命令和查詢之間不需要額外的互動,命令側的資料更新對查詢側實時可見。在這種架構模式下,兩側基於共享的資料已經天然的整合在一起,不需要額外機制進行通訊,自然也無需引入訊息了。如果我們採用CQRS模式,並且命令和查詢兩側進行了資料模型的分離,二者各自依賴獨立的資料模型。同時,資料儲存也分開部署。命令側負責資料的更新,而查詢側只負責資料的查詢,如何將資料的更新及時同步到查詢側是需要解決的問題。在這種架構模式下,使用訊息模式作為兩側的通訊機制是個不錯的選擇,當然,這並不是唯一的選項。

2.3 CQRS 和 ES(Event Sourcing, 事件溯源)

ES 並不是一個新的概念,在最早的金融系統中就已經應用。要了解ES,我們需要先看看傳統的資料儲存。在傳統應用中,資料庫例如MySQL(假設儲存介質是資料庫,)中儲存的始終是資料的最新的狀態。例如我們對某條使用者的資訊進行了多次的修改或編輯,然後儲存將資料儲存到資料庫中。無論何時,資料庫中都會記錄最後的、最新的使用者狀態。我們只要根據id或其他資訊查詢資料庫中相應的記錄就能獲取該使用者的最新資訊。這是應用中典型的資料儲存特點。

當然,我們可以基於特定的資料模型設計以儲存資料的更改記錄。

這種資料儲存模式的特點是簡單,不需要額外的維護複雜的設計,我們能夠非常容易的獲取最新的使用者資訊。但是不幸的是,我們丟失了歷史資訊,包括使用者的意圖資訊。而這些資訊則有助於我們進行資料回滾、使用者行為分析以及開發過程中的除錯等等。

 

 

在ES模式下,資料庫中儲存的不在是資料最新狀態,而是資料的變更記錄,更官方的說法是 “事件(Event)”。資料庫中儲存的資料變化的事件流。我們基於事件流可以對最新狀態進行重建,同時也可以便捷的重現任何歷史節點資料。ES需要解決大量事件的儲存和高效的例項重建問題,後續單獨的文章再介紹ES。

2.4 CQRS 和 Eventual Consistency(最終一致性)

最終一致性也常常在服務之間引入,最終一致性的目的是為了提高擴充套件性和可用性。

CQRS和最終一致性同樣沒有必然的聯絡。往往採用CQRS後,查詢和命令兩側會採用獨立的資料模型,在這種架構模式下,命令側的資料變化後及時同步到查詢側,兩側資料並非實時,在一定的延時後兩側資料最終達成一致。

3 結語

CQRS的最大優勢在於透過將命令和查詢的職責分離,為架構師提供了更多的架構屬性選擇,我們可以在查詢側和命令側進行獨立的架構設計。物件級別的職責分離就是CQRS的全部了,但在實踐中湧現出了很多更為靈活也更為複雜的架構風格,比如匯流排的引入、資料模型的分離、一致性報這個策略、事件溯源等等。額外的元件或技術的引入必然導致複雜性和成本上升,這些選型的採納需要團隊的權衡。

相關文章