命令和查詢責任隔離(CQRS)模式

【博文目錄>>>】


命令和查詢責任隔離(CQRS)模式

使用單獨的接口將讀取數據的操作與更新數據的操作隔離開來。這種模式可以最大限度地提高性能、可伸縮性和安全性;通過更高的靈活性支持系統隨時間的發展;並防止更新命令在域級別造成合並衝突。

背景與問題

在傳統的數據管理系統中,命令(對數據的更新)和查詢(對數據的請求)都針對單個數據存儲庫中的同一組實體執行。這些實體可以是關係數據庫(例如SQL Server)中一個或多個表中行的子集。

通常,在這些系統中,所有創建、讀取、更新和刪除(CRUD)操作都應用於實體的相同表示形式。例如,數據訪問層(DAL)從數據存儲中檢索表示客戶的數據傳輸對象(DTO),並顯示在屏幕上。用戶更新DTO的某些字段(可能通過數據綁定),然後DTO被DAL保存回數據存儲中。相同的DTO用於讀寫操作,如圖1所示。

在這裏插入圖片描述

圖1-傳統CRUD體系結構

當應用於數據操作的業務邏輯有限時,傳統的CRUD設計工作良好。開發工具提供的腳手架機制可以非常快速地創建數據訪問代碼,然後可以根據需要定製這些代碼。

然而,傳統的CRUD方法有一些缺點:

  • 它通常意味着數據的讀和寫表示之間不匹配,例如,即使操作中不需要它們,也必須正確更新其他列或屬性。
  • 當數據存儲中鎖定記錄時,可能會遇到協作域中的數據爭用(多個參與者在同一組數據上並行操作),或者在使用樂觀鎖定時由併發更新引起的更新衝突。這些風險隨着系統的複雜性和吞吐量的增加而增加。此外,由於數據存儲和數據訪問層的負載以及檢索信息所需查詢的複雜性,傳統方法也會對性能產生負面影響。
  • 它會使管理安全性和權限變得更麻煩,因爲每個實體都要同時執行讀和寫操作,這可能會無意中暴露錯誤上下文中的數據。


要更深入地瞭解CRUD方法的侷限性,請參閱MSDN上的"CRUD,只有在您能夠負擔的情況下"在MSDN上。

解決方案

命令和查詢責任隔離(CQRS)是一種模式,它通過使用單獨的接口將讀取數據(查詢)的操作與更新數據(命令)的操作隔離開來。這意味着用於查詢和更新的數據模型是不同的。然後可以隔離模型,如圖2所示,儘管這不是絕對的需求。

在這裏插入圖片描述

圖2-基本的CQRS體系結構

與基於CRUD的系統中固有的數據模型(開發人員從中構建自己的概念模型)相比,對基於CQRS的系統中的數據使用單獨的查詢和更新模型大大簡化了設計和實現。但是,一個缺點是,與CRUD設計不同,CQRS代碼不能通過使用腳手架機制自動生成。

用於讀取數據的查詢模型和用於寫入數據的更新模型可以訪問相同的物理存儲,可以使用SQL視圖或動態生成投影。但是,通常將數據分成不同的物理存儲以最大化性能,可伸縮性和安全性。如圖3所示。

在這裏插入圖片描述

圖3-具有單獨讀寫存儲的CQRS體系結構

讀存儲可以是寫存儲的只讀副本,或者讀和寫存儲可能具有完全不同的結構。使用讀取存儲的多個只讀副本可以顯著提高查詢性能和應用程序UI響應能力,特別是在分佈場景中,其中只讀副本位於接近應用程序實例的位置。一些數據庫系統(如SQL Server)提供了其他功能,如故障轉移副本,以最大限度地提高可用性。

讀和寫存儲的分離也允許每個存儲被適當地縮放以匹配負載。例如,讀存儲通常會遇到比寫入存儲高得多的負載。

當查詢/讀取模型包含非規範化信息時(請參見物化視圖模式),在爲應用程序中的每個視圖讀取數據或查詢系統中的數據時,性能都會最大化。

有關CQRS模式及其實現的更多信息,請參見以下資源:

問題和思考

在決定如何實現此模式時,請考慮以下幾點:

  • 將數據存儲劃分爲單獨的物理存儲進行讀和寫操作可以提高系統的性能和安全性,但它可以在彈性和最終一致性方面增加相當大的複雜性。必須更新讀取模型存儲,以反映對寫入模型存儲的更改,並且可能難以檢測用戶何時基於過時的讀取數據發出了請求,這意味着操作無法完成。


有關最終一致性的說明,請參見"數據一致性入門"

  • 考慮將CQRS應用於系統中最有價值的有限部分,並從中吸取經驗教訓。
  • 實現最終一致性的一種典型方法是將事件源與CQRS結合使用,這樣寫模型就是由命令執行驅動的僅附加的事件流。這些事件用於更新作爲讀取模型的物化視圖。有關更多信息,請參見事件源和CQRS。

何時使用此模式

這種模式非常適合於:

  • 在同一數據上並行執行多個操作的協作域。CQRS允許您定義具有足夠粒度的命令,以儘量減少域級別的合併衝突(或任何確實出現的衝突都可以由命令合併),即使在更新看起來是相同類型的數據時也是如此。
  • 與基於任務的用戶界面(其中,通過一系列步驟指導用戶完成一個複雜的過程),複雜的域模型以及已經熟悉域驅動設計(DDD)技術的團隊一起使用寫入模型具有完整的命令處理堆棧,其中包含業務邏輯,輸入驗證和業務驗證,以確保每個聚合始終保持一致(每個關聯對象的羣集都被視爲一個單元,以進行數據更改)。讀取模型沒有業務邏輯或驗證堆棧,僅返回DTO以在視圖模型中使用。讀取模型最終與寫入模型一致。
  • 數據讀取的性能必須與數據寫入的性能分開微調的場景,特別是當讀寫比很高時,以及當需要水平縮放時。例如,在許多系統中,讀取操作的數量比寫入操作的數量大幾個數量級。爲此,請考慮擴展讀取模型,但僅在一個或幾個實例上運行寫入模型。少量的寫模型實例還有助於最大程度地減少合併衝突的發生。
  • 一個團隊的開發人員可以專注於作爲寫模型部分的複雜域模型,而另一組經驗不足的團隊可以專注於讀模型和用戶界面的場景。
  • 預計系統會隨着時間而發展並且可能包含模型的多個版本,或者業務規則會定期更改的場景。
  • 與其他系統的集成,特別是與事件源的組合,其中一個子系統的時間故障不會影響其他子系統的可用性。

這種模式可能不適用於以下情況:

  • 領域或業務規則很簡單的地方。
  • 簡單的CRUD風格的用戶界面和相關的數據訪問操作就足夠了。
  • 用於整個系統的實施。總體數據管理方案中有一些特定的組件,在這些組件中CQRS可能有用,但是在實際上不需要的情況下,它可能會增加相當多的不必要的複雜性。

事件源與CQRS

CQRS模式通常與事件源模式一起使用。基於CQRS的系統使用獨立的讀寫數據模型,每個模型都是根據相關任務量身定做的,並且通常位於物理上獨立的存儲區。當與事件源一起使用時,事件的存儲是寫模型,這是信息的權威來源。基於CQRS的系統的讀取模型提供數據的物化視圖,通常是高度非規範化的視圖。這些視圖是根據應用程序的接口和顯示需求定製的,這有助於最大限度地提高顯示和查詢性能。

使用事件流作爲寫存儲而不是某個時間點的實際數據,可以避免在單個聚合上發生更新衝突,並最大限度地提高性能和可伸縮性。這些事件可用於異步生成用於填充讀取存儲的數據的物化視圖。

因爲事件存儲是信息的權威來源,所以可以在系統演進或必須更改讀取模型時刪除實例化視圖並重播所有過去的事件,以創建當前狀態的新表示形式。物化視圖實際上是數據的持久只讀緩存。。

當使用CQRS與事件源模式相結合時,請考慮以下幾點:

  • 與任何讀寫存儲分離的系統一樣,基於這種模式的系統最終只能是最終一致的。在生成的事件與保存由這些事件更新的操作結果的數據存儲之間會有一些延遲。。
  • 該模式帶來了額外的複雜性,因爲必須創建代碼來初始化和處理事件,以及組裝或更新查詢或讀取模型所需的適當視圖或對象。當將CQRS模式與事件源結合使用時,其固有的複雜性會使成功實施變得更加困難,並且需要重新學習一些概念和設計系統的不同方法。但是,由於保留了數據更改的意圖,因此事件源可以使對域的建模更加容易,並且可以更輕鬆地重建視圖或創建新視圖。
  • 通過重放和處理特定實體或實體集合的事件來生成物化視圖以用於讀取的模型或數據的投影,可能需要大量的處理時間和資源使用,尤其是在需要長時間彙總或分析值的情況下,因爲可能需要檢查所有相關事件。通過按計劃的時間間隔實施數據快照(例如已發生的特定操作的總數或實體的當前狀態),可以部分解決此問題。


有關更多信息,請參見在MSDN上事件源模式物化視圖模式,以及模式和實踐指南CQRS旅程。特別是你應該閱讀這一章。事件源簡介以充分探討該模式及其如何在CQRS中使用,以及本章CQRS與ES深度歷險瞭解更多–包括如何在MicrosoftAzure中與CQRS一起使用聚合分區。

示例

下面的代碼顯示了CQRS實現示例中的一些摘錄,CQRS實現對讀和寫模型使用了不同的定義。模型接口並不規定底層數據存儲的任何特性,它們可以獨立地進化和調整,因爲這些接口是分開的。

下面的代碼顯示了讀取模型定義。

// Query 
interface namespace ReadModel { 
    public interface ProductsDao { 
        ProductDisplay FindById(int productId); 
        IEnumerable<ProductDisplay> FindByName(string name); IEnumerable<ProductInventory> FindOutOfStockProducts(); IEnumerable<ProductDisplay> 
        FindRelatedProducts(int productId); 
    } 
    
    public class ProductDisplay { 
        public int ID { get; set; } 
        public string Name { get; set; } 
        public string Description { get; set; } 
        public decimal UnitPrice { get; set; } 
        public bool IsOutOfStock { get; set; } 
        public double UserRating { get; set; } 
    } 
    public class ProductInventory { 
        public int ID { get; set; } 
        public string Name { get; set; } 
        public int CurrentStock { get; set; } 
    } 
}

該系統允許用戶對產品進行評分。應用程序代碼通過使用以下代碼中顯示的RateProduct命令來執行此操作。

public interface Icommand{  
    Guid Id { get; }
}

public class RateProduct : Icommand{  
    public RateProduct()  { this.Id = Guid.NewGuid(); }  
    public Guid Id { get; set; }  
    public int ProductId { get; set; }  
    public int rating { get; set; }  
    public int UserId {get; set; }
}

系統使用ProductsCommandHandler類來處理應用程序發送的命令。 客戶端通常通過消息系統(例如隊列)將命令發送到域。 命令處理程序接受這些命令並調用域接口的方法。 每個命令的粒度旨在減輕衝突請求的機會。 以下代碼顯示了ProductsCommandHandler類的概述。

public class ProductsCommandHandler : 
        ICommandHandler<AddNewProduct>, ICommandHandler<RateProduct>,    ICommandHandler<AddToInventory>,ICommandHandler<ConfirmItemShipped>,    ICommandHandler<UpdateStockFromInventoryRecount> { 
    private readonly IRepository<Product> repository;  
    public ProductsCommandHandler (IRepository<Product> repository)  {
        this.repository = repository;  
    }  
    void Handle (AddNewProduct command){ ... }  
    void Handle (RateProduct command)  {    
        var product = repository.Find(command.ProductId);    
        if (product != null)    {      
            product.RateProuct(command.UserId, command.rating);      repository.Save(product);    
        }  
    }  
    
    void Handle (AddToInventory command)  { ... } 
    void Handle (ConfirmItemsShipped command)  { ... }  
    void Handle (UpdateStockFromInventoryRecount command)  { ... }
}

以下代碼顯示了寫入模型中的ProductsDoman接口。

public interface ProductsDomain {
    void AddNewProduct(int id, string name, string description, decimal price);
    void RateProduct(int userId int rating);
    void AddToInventory(int productId, int quantity);
    void ConfirmItemsShipped(int productId, int quantity);
    void UpdateStockFromInventoryRecount(int productId, int updatedQuantity);
}

還要注意ProductsDomain接口如何包含在域中具有意義的方法。通常,在CRUD環境中,這些方法將具有通用名稱(如“Save”或“Update”),並且將DTO作爲唯一參數。可以更好地定製CQRS方法,以適合該組織執行業務和庫存管理的方式。

相關模式和指導

在實施這一模式時,下列模式和指導也可能相關:

  • 數據一致性入門。本指南介紹了使用CQRS模式時由於讀寫數據存儲之間最終的一致性而通常遇到的問題,以及如何解決這些問題。
  • 數據分區指南。本指南介紹瞭如何將CQRS模式中使用的讀寫數據存儲區劃分爲多個單獨的分區,這些分區可以分別進行管理和訪問,以提高可伸縮性,減少爭用並優化性能。
  • 事件源模式。此模式更詳細地描述瞭如何將事件源與CQRS模式一起使用以簡化複雜域中的任務。改善性能,可伸縮性和響應能力;提供交易數據的一致性;並保持完整的審計追蹤和歷史記錄,以支持採取補償措施。
  • 物化視圖模式。CQRS實現的讀取模型可以包含寫入模型數據的實例化視圖,或者讀取模型可以用於生成實例化視圖。

更多信息

  • MSDN上模式與實踐指南CQRS之旅
  • Martin Fowler博客上的文章CQRS
  • Greg Young在Code Better網站上的文章。

原文鏈接

https://docs.microsoft.com/en-us/previous-versions/msp-n-p/dn568103%28v%3dpandp.10%29

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章