(六)——命令和查詢職責(zé)分離(CQRS)模式

2018-02-24 15:44 更新

云計(jì)算設(shè)計(jì)模式(六)——命令和查詢職責(zé)分離(CQRS)模式

隔離,通過使用不同的接口,從操作讀取數(shù)據(jù)更新數(shù)據(jù)的操作。這種模式可以最大限度地提高性能,可擴(kuò)展性和安全性;支持系統(tǒng)在通過較高的靈活性,時(shí)間的演變;防止更新命令,從造成合并在域級(jí)別上的沖突。

背景和問題

在傳統(tǒng)的數(shù)據(jù)管理系統(tǒng)中,這兩個(gè)命令(更新數(shù)據(jù))和查詢(請(qǐng)求數(shù)據(jù)),針對(duì)在一個(gè)單一的數(shù)據(jù)存儲(chǔ)庫(kù)中的相同的一組實(shí)體的執(zhí)行。這些實(shí)體可以是在關(guān)系數(shù)據(jù)庫(kù)中的一個(gè)或多個(gè)表,如 SQL Server 的行的子集。

典型地,在這些系統(tǒng)中,所有的創(chuàng)建,讀取,更新和刪除(CRUD)操作被施加到該實(shí)體的相同的表示。例如,一個(gè)數(shù)據(jù)傳輸對(duì)象(DTO)的代表顧客從數(shù)據(jù)存儲(chǔ)中檢索由數(shù)據(jù)訪問層(DAL)并顯示在屏幕上。用戶更新 DTO 的某些領(lǐng)域(也許是通過數(shù)據(jù)綁定)和 DTO,然后保存回?cái)?shù)據(jù)存儲(chǔ)在 DAL。相同的 DTO 同時(shí)用于讀取和寫入操作,如圖1所示。

圖1 - 一個(gè)傳統(tǒng)的 CRUD 架構(gòu)

傳統(tǒng)的 CRUD 設(shè)計(jì)工作良好時(shí),只有施加到數(shù)據(jù)操作有限的業(yè)務(wù)邏輯。由開發(fā)工具提供可以非??焖俚貏?chuàng)建數(shù)據(jù)訪問代碼的支架機(jī)構(gòu),根據(jù)需要,可再進(jìn)行定制。

然而,傳統(tǒng)的 CRUD 方法有一些缺點(diǎn):

  • 它往往意味著存在所述讀取和寫入的數(shù)據(jù),如額外的列或?qū)傩?,即使它們不是必需的作為操作的一部分,必須正確地更新的表示之間的不匹配。
  • 它遇到風(fēng)險(xiǎn)的數(shù)據(jù)爭(zhēng)用一個(gè)協(xié)作領(lǐng)域(在多個(gè)參與者??并行運(yùn)行在相同的數(shù)據(jù)集)時(shí),記錄被鎖定在數(shù)據(jù)存儲(chǔ),或者更新沖突所造成的并發(fā)更新時(shí),樂觀鎖使用。這些風(fēng)險(xiǎn)增加的復(fù)雜性和系統(tǒng)的吞吐量增加。此外,傳統(tǒng)的方法也可以對(duì)性能有負(fù)面影響,由于加載的數(shù)據(jù)存儲(chǔ)和數(shù)據(jù)訪問層上,并在檢索信息需要查詢的復(fù)雜度。
  • 它可以使安全管理和權(quán)限比較繁瑣,因?yàn)槊恳粋€(gè)實(shí)體是受讀取和寫入操作,這可能會(huì)在不經(jīng)意間暴露的數(shù)據(jù)在錯(cuò)誤的情況下。

注意: 對(duì)于的 CRUD 方法的局限性有了更深的了解請(qǐng)參見“CRUD,只有當(dāng)你能負(fù)擔(dān)得起”MSDN 上。

解決方案

命令和查詢職責(zé)分離(CQRS)是偏析,通過使用獨(dú)立的接口讀取操作的更新數(shù)據(jù)(命令)的數(shù)據(jù)(查詢)的操作模式。這意味著,用于查詢和更新的數(shù)據(jù)模型是不同的。該模型可隨后被分離,如在圖 2 中,雖然這不是絕對(duì)的要求。

圖2 - 一個(gè)基本的 CQRS 架構(gòu)

相比于數(shù)據(jù)(從該開發(fā)商建立自己的概念模式)的單個(gè)模型中固有的 CRUD 為基礎(chǔ)的系統(tǒng)中,使用單獨(dú)的查詢和更新模型中 CQRS 為基礎(chǔ)的系統(tǒng)中的數(shù)據(jù)顯著地簡(jiǎn)化設(shè)計(jì)和實(shí)施。然而,一個(gè)缺點(diǎn)是,不像 CRUD 的設(shè)計(jì),CQRS 代碼不能自動(dòng)用支架的機(jī)制產(chǎn)生。

查詢模型讀取數(shù)據(jù)和寫入數(shù)據(jù)可以訪問相同的實(shí)體店,也許是通過使用 SQL 視圖的更新模型,或產(chǎn)生對(duì)飛預(yù)測(cè)。但是,它是常見的數(shù)據(jù)分成不同的物理存儲(chǔ)來(lái)提高性能,可擴(kuò)展性和安全性;如圖3。

圖3 - 一個(gè) CQRS 架構(gòu),具有獨(dú)立讀寫店

所讀取的存儲(chǔ)可以是只讀副本寫入存儲(chǔ)區(qū),或讀取和寫入存儲(chǔ)可以具有不同的結(jié)構(gòu)完全。使用 read 店的多個(gè)只讀副本可以大大提高查詢性能和應(yīng)用程序的UI響應(yīng)速度,尤其是在分布式場(chǎng)景下的只讀副本靠近應(yīng)用程序?qū)嵗?。一些?shù)據(jù)庫(kù)系統(tǒng),如 SQL Server,提供額外的功能,如故障轉(zhuǎn)移副本,以最大限度地提高可用性。

讀的分離和寫入存儲(chǔ)還允許每個(gè)到會(huì)適當(dāng)縮放以匹配負(fù)載。例如,讀取存儲(chǔ)通常會(huì)遇到一個(gè)更高的負(fù)載寫入存儲(chǔ)。

當(dāng)查詢/讀取模型中包含的非規(guī)范化的信息(見物化視圖模式),性能正在讀取數(shù)據(jù)的每一個(gè)視圖時(shí)在應(yīng)用程序中或在查詢系統(tǒng)中的數(shù)據(jù)時(shí)最大化。

有關(guān)CQRS模式及其實(shí)現(xiàn)的詳細(xì)信息,請(qǐng)參閱以下資源:

  • 該模式與實(shí)踐指導(dǎo) CQRS 之旅 MSDN 上。尤其是你應(yīng)該閱讀的章節(jié)介紹了命令查詢職責(zé)分離方式進(jìn)行全面的探索模式,當(dāng)它是有用的,這一章尾聲:經(jīng)驗(yàn)教訓(xùn),了解一些,可以使用這種模式時(shí)出現(xiàn)的問題。
  • 該職位 CQRS 由馬丁·福勒,這也解釋了該模式的基本知識(shí),并鏈接到其他一些有用的資源。
  • 代碼更好的網(wǎng)站,它探討的 CQRS 模式的許多方面對(duì) Greg Young 的帖子。

問題和注意事項(xiàng)

在決定如何實(shí)現(xiàn)這個(gè)模式時(shí),請(qǐng)考慮以下幾點(diǎn):

  • 分割數(shù)據(jù)存儲(chǔ)到單獨(dú)的物理存儲(chǔ)用于讀操作和寫操作可以提高系統(tǒng)的性能和安全性,但它可以在彈性和最終一致性方面增加了相當(dāng)大的復(fù)雜性。所讀取的模型存儲(chǔ)必須被更新以反映變化的寫入模型存儲(chǔ),并且它可以是難以檢測(cè)用戶何時(shí)發(fā)出基于讀取過時(shí)數(shù)據(jù)意味著該操作不能完成的請(qǐng)求。

注意

對(duì)于最終一致性的說明,請(qǐng)參閱數(shù)據(jù)一致性底漆。

  • 考慮 CQRS 應(yīng)用到你的系統(tǒng)中的限制部分地方將是最有價(jià)值的,并從經(jīng)驗(yàn)中學(xué)習(xí)。
  • 的典型方法擁抱最終一致性是使用事件采購(gòu)與 CQRS 結(jié)合使寫模式是由執(zhí)行命令的驅(qū)動(dòng)事件的追加只流。這些事件被用來(lái)更新充當(dāng)讀取模型化視圖。欲了解更多信息,請(qǐng)參閱事件獲取和 CQRS。

當(dāng)使用這個(gè)模式

這種模式非常適合于:

  • 其中并行地對(duì)相同的數(shù)據(jù)進(jìn)行多項(xiàng)操作協(xié)同域。 CQRS 允許你有足夠的粒度定義的命令,以盡量減少在域級(jí)別(或者不出現(xiàn)可以通過在命令合并的沖突)的合并沖突,更新這似乎是同一類型的數(shù)據(jù)時(shí)也是如此。
  • 使用與基于任務(wù)的用戶界面(其中用戶通過一個(gè)復(fù)雜的過程引導(dǎo)作為一系列步驟),具有復(fù)雜的領(lǐng)域模型,以及用于團(tuán)隊(duì)已經(jīng)熟悉領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(DDD)技術(shù)。在寫入模式有一個(gè)完整的命令處理?xiàng)Ec業(yè)務(wù)邏輯,輸入驗(yàn)證和業(yè)務(wù)驗(yàn)證,以確保一切總是為每個(gè)聚集體(被視為一個(gè)單元進(jìn)行數(shù)據(jù)變更的目的相關(guān)聯(lián)的對(duì)象的每個(gè)集群相一致)中的寫入模式。讀出的模型沒有業(yè)務(wù)邏輯或驗(yàn)證的堆棧,只是返回一個(gè) DTO 在一個(gè)視圖模型的使用。讀出的模型與模型寫入最終一致。
  • 方案,其中數(shù)據(jù)的讀出性能,必須分別從數(shù)據(jù)的性能進(jìn)行微調(diào)寫入,尤其是當(dāng)讀/寫比是非常高的,并且當(dāng)水平擴(kuò)展是必要的。例如,在許多系統(tǒng)中的讀取操作的數(shù)目是幾個(gè)數(shù)量級(jí)更大的寫入操作的數(shù)目。為了適應(yīng)這種情況,考慮向外擴(kuò)展的讀取模式,但只在一個(gè)或幾個(gè)實(shí)例中運(yùn)行的寫模式。少數(shù)寫入模型實(shí)例也有助于減少合并沖突的發(fā)生。
  • 場(chǎng)景的開發(fā)者之一的團(tuán)隊(duì)可以專注于復(fù)雜的領(lǐng)域模型,它是寫模型的一部分??,而另一個(gè)經(jīng)驗(yàn)不足的團(tuán)隊(duì)可以專注于讀模型和用戶界面。
  • 場(chǎng)景中,預(yù)計(jì)隨著時(shí)間的推移,系統(tǒng),并且可以包含多個(gè)版本的模型,或者業(yè)務(wù)規(guī)則經(jīng)常改變。
  • 與其他系統(tǒng),特別是與事件采購(gòu),其中一個(gè)子系統(tǒng)的瞬時(shí)故障不會(huì)影響到其它的可用性的組合一體化。

這種模式可能不適合于下列情況:

  • 凡域或業(yè)務(wù)規(guī)則很簡(jiǎn)單。
  • 凡一個(gè)簡(jiǎn)單的 CRUD 風(fēng)格的用戶界面和相關(guān)的數(shù)據(jù)訪問操作就足夠了。
  • 對(duì)于在整個(gè)系統(tǒng)中的實(shí)現(xiàn)。有一個(gè)整體的數(shù)據(jù)管理方案,其中 CQRS 可以是有用的特定組件,但是它可以增加它實(shí)際上并不需要相當(dāng)大的和往往是不必要的復(fù)雜性。

事件獲取和 CQRS

CQRS 模式常用于與事件獲取圖案一起使用。 CQRS 為基礎(chǔ)的系統(tǒng)使用分離的讀取和寫入的數(shù)據(jù)模型,每個(gè)針對(duì)有關(guān)任務(wù)和通常位于物理上分離的存儲(chǔ)區(qū)。當(dāng)與采購(gòu)活動(dòng)時(shí),事件的存儲(chǔ)是寫模式,這是信息的權(quán)威來(lái)源。一個(gè) CQRS 為基礎(chǔ)的系統(tǒng)的讀取模型提供數(shù)據(jù)的物化視圖,通常是高度非規(guī)范化的意見。這些視圖量身定做的接口和應(yīng)用程序,這有助于最大程度地顯示和查詢性能的顯示要求。

使用事件作為寫入存儲(chǔ)區(qū),而不是實(shí)際的數(shù)據(jù)的流,在一個(gè)時(shí)間點(diǎn),避免了在單個(gè)聚合更新沖突并最大限度地提高性能和可擴(kuò)展性。該事件可用于異步生成用于填充讀取存儲(chǔ)器中的數(shù)據(jù)的實(shí)體化視圖。

由于事件存儲(chǔ)是信息的權(quán)威來(lái)源,就可以刪除物化視圖和回放所有過去的事件來(lái)創(chuàng)建當(dāng)前狀態(tài)的一個(gè)新表示當(dāng)系統(tǒng)升級(jí)時(shí),或者當(dāng)讀取模式必須改變。物化視圖是有效的數(shù)據(jù)的耐用只讀緩存。

當(dāng)使用 CQRS 結(jié)合事件獲取模式,考慮以下幾點(diǎn):

  • 與任何系統(tǒng),其中寫入和讀出存儲(chǔ)是分開的,在此基礎(chǔ)上圖案系統(tǒng)唯一最終一致。將有被生成的事件和數(shù)據(jù)存儲(chǔ)器保存由這些事件被更新啟動(dòng)操作的結(jié)果之間有一些延遲。
  • 該模式引入由于代碼必須創(chuàng)建啟動(dòng)和處理事件,并組裝或者更新查詢或讀取模型所需的適當(dāng)?shù)囊庖娀蛭矬w額外的復(fù)雜性。在采購(gòu)活動(dòng)一起使用的 CQRS 模式固有的復(fù)雜性時(shí),可以做一個(gè)成功的實(shí)現(xiàn)更加困難,需要重新學(xué)習(xí)的一些概念和不同的方法來(lái)設(shè)計(jì)系統(tǒng)。然而,事件采購(gòu)可以更容易地對(duì)域進(jìn)行建模,并且可以更容易地重建的觀點(diǎn)或創(chuàng)建新的,因?yàn)樽兓臄?shù)據(jù)的意圖將被保留。
  • 生成物化視圖中讀取模型或數(shù)據(jù)通過重放和處理為特定的實(shí)體或?qū)嶓w的集合的事件突起的使用可能需要相當(dāng)多的處理時(shí)間和資源的使用,尤其是如果它需要求和或值的數(shù)據(jù)在長(zhǎng)時(shí)間內(nèi)的,因?yàn)樗械南嚓P(guān)聯(lián)的事件可能需要被審查。這可以通過實(shí)現(xiàn)數(shù)據(jù)的快照在預(yù)定的時(shí)間間隔,如已經(jīng)發(fā)生的特定操作的次數(shù),或一個(gè)實(shí)體的當(dāng)前狀態(tài)的總計(jì)數(shù)被部分地解決。

注意: 欲了解更多信息,請(qǐng)參閱活動(dòng)采購(gòu)模式和物化視圖模式,以及模式與實(shí)踐指導(dǎo) CQRS 之旅 MSDN 上。尤其是你應(yīng)該閱讀的章節(jié)介紹采購(gòu)活動(dòng)進(jìn)行全面的探索模式,以及它如何與 CQRS 有用的,而章 CQRS 和 ES 深潛了解更多,包括如何聚集分區(qū)可以在微軟的 Azure CQRS 使用。

例子

下面的代碼顯示了一個(gè) CQRS 實(shí)現(xiàn),它使用不同的定義讀取和寫入模型為例某些提取物。該模型的接口沒有規(guī)定的基礎(chǔ)數(shù)據(jù)存儲(chǔ)的任何功能,并且可以發(fā)展和進(jìn)行微調(diào)獨(dú)立,因?yàn)檫@些接口是分開的。

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

// 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; }  
  }  
}

該系統(tǒng)允許用戶率的產(chǎn)品。應(yīng)用程序代碼通過使用在下面的代碼中所示的 RateProduct 命令執(zhí)行此操作。

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; }  
}

本系統(tǒng)采用 ProductsCommandHandler 類來(lái)處理由應(yīng)用程序發(fā)出的命令。客戶端通常通過消息傳送系統(tǒng)發(fā)送命令到域,如一個(gè)隊(duì)列。命令處理程序接受這些命令,并調(diào)用域接口的方法。每個(gè)命令的粒度被設(shè)計(jì)成減輕沖突請(qǐng)求的機(jī)會(huì)。下面的代碼顯示了 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 接口包含在域中的意義的方法。通常情況下,在一個(gè) CRUD 環(huán)境中,這些方法將有通用名稱,如保存或更新,并有一個(gè) DTO 作為唯一的參數(shù)。該 CQRS 方法可以更好地定制,以滿足該組織開展業(yè)務(wù)及庫(kù)存管理的方式。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)