首页 > 代码库 > 云计算设计模式(六)——命令和查询职责分离(CQRS)模式

云计算设计模式(六)——命令和查询职责分离(CQRS)模式

云计算设计模式(六)——命令和查询职责分离CQRS模式


隔离,通过使用不同的接口,从操作读取数据更新数据的操作。这种模式可以最大限度地提高性能,可扩展性和安全性;支持系统通过较高的灵活性,时间演变;防止更新命令,从造成合并在域级别上的冲突。


背景和问题


传统的数据管理系统中,这两个命令更新数据查询(请求数据),针对一个单一的数据存储库中的相同的一组实体的执行。这些实体可以是关系数据库中的一个或多个表,如SQL Server行的子集

典型地,在这些系统中,所有的创建,读取,更新和删除(CRUD)操作被施加到该实体的相同的表示例如一个数据传输对象DTO)的代表顾客从数据存储中检索由数据访问DAL)显示在屏幕上用户更新DTO的某些领域也许是通过数据绑定)和DTO,然后保存数据存储DAL相同的DTO同时用于读取和写入操作图1所示

图1 - 一个传统的CRUD架构


传统的CRUD设计工作良好,只有施加到数据操作有限的业务逻辑。开发工具提供可以非常快速地创建数据访问代码的支架机构根据需要,可进行定制。

然而,传统的CRUD方法有一些缺点:
?它往往意味着存在所述读取和写入数据额外的列属性,即使它们不是必需的作为操作的一部分,必须正确地更新表示之间的不匹配。
?它遇到风险的数据争用一个协作领域在多个参与者??并行运行在相同的数据集时,记录被锁定在数据存储或者更新冲突所造成的并发更新时,乐观锁使用这些风险增加复杂性系统的吞吐量增加。此外,传统的方法可以对性能有负面影响,由于加载的数据存储和数据访问检索信息需要查询的复杂度。
?它可以使安全管理和权限比较繁琐,因为每一个实体是受读取和写入操作这可能会在不经意间暴露的数据在错误的情况下


注意:

对于CRUD方法的局限性有了更深的了解请参见“CRUD,只有当你能负担得起MSDN上


解决方案


命令和查询职责分离CQRS偏析,通过使用独立的接口读取操作更新数据命令)的数据查询操作模式这意味着,用于查询和更新数据模型是不同的。模型可随后被分离,在图2中,虽然这不是绝对的要求

图2 - 一个基本的CQRS架构


相比于数据从该开发商建立自己的概念模式单个模型中固有的CRUD为基础的系统中,使用单独的查询和更新模型CQRS为基础的系统中的数据显着地简化设计和实施。然而,一个缺点是,不像CRUD的设计,CQRS代码不能自动支架的机制产生

查询模型读取数据写入数据可以访问相同的实体店也许是通过使用SQL视图更新模型,或产生对飞预测。但是,它是常见的数据分成不同的物理存储来提高性能可扩展性和安全性;如图3

图3 - 一个CQRS架构,具有独立读写


所读取的存储可以是只读副本写入存储区读取和写入存储可以具有不同的结构完全使用read多个只读副本可以大大提高查询性能应用程序的UI响应速度,尤其是在分布式场景下的只读副本靠近应用程序实例一些数据库系统如SQL Server提供额外的功能,如故障转移副本,以最大限度地提高可用性

的分离和写入存储还允许每个会适当缩放以匹配负载。例如读取存储通常会遇到一个更高的负载写入存储

当查询/读取模型中包含的非规范化的信息(见物化视图模式),性能正在读取数据的每一个视图时在应用程序中在查询系统中的数据最大化。

有关CQRS模式及其实现的详细信息请参阅以下资源
?模式与实践指导CQRS之旅MSDN上尤其是你应该阅读的章节介绍命令查询职责分离方式进行全面的探索模式,当它是有用的,这一章尾声:经验教训,了解一些,可以使用这种模式出现的问题
?该职位CQRS马丁·福勒这也解释了该模式的基本知识,并链接到其他一些有用的资源
?代码更好的网站探讨CQRS模式的许多方面Greg Young的帖子


问题和注意事项


在决定如何实现这个模式时,请考虑以下几点
?分割数据存储到单独的物理存储用于操作和写操作可以提高系统的性能安全性,但它可以弹性和最终一致性方面增加了相当大的复杂性。所读取的模型存储必须被更新以反映变化的写入模型存储,并且它可以是难以检测用户何时发出基于读取过时数据意味着该操作不能完成的请求。


注意:

对于最终一致性的说明,请参阅数据一致性底漆


?考虑CQRS应用到你的系统的限制部分地方将是最有价值的并从经验中学习
?的典型方法拥抱最终一致性是使用事件采购CQRS结合使写模式是执行命令的驱动事件追加流。这些事件被用来更新充当读取模型化视图欲了解更多信息,请参阅事件获取CQRS


使用这个模式


这种模式非常适合于:
?其中并行地对相同的数据进行多项操作协同域。CQRS允许你足够的粒度定义的命令,以尽量减少在域级别(或者出现可以通过在命令合并的冲突)的合并冲突,更新这似乎是同一类型的数据时也是如此。
?使用基于任务的用户界面(其中用户通过一个复杂的过程引导作为一系列步骤,具有复杂的领域模型,以及用于团队已经熟悉领域驱动设计(DDD)技术。在写入模式有一个完整的命令处理与业务逻辑,输入验证业务验证,以确保一切总是为每个聚集体视为一个单元进行数据变更的目的相关联的对象每个集群相一致的写入模式。读出的模型没有业务逻辑或验证的堆栈,只是返回一个DTO一个视图模型的使用读出的模型与模型写入最终一致
?方案,其中数据读出性能,必须分别数据性能进行微调写入,尤其是当读/写是非常高的,并且当水平扩展是必要的。例如在许多系统中的读取操作的数目是几个数量级更大的写入操作的数目为了适应这种情况考虑向外扩展的读取模式,但只在一个几个实例中运行的写模式。少数写入模型实例也有助于减少合并冲突的发生。
?场景开发者之一的团队可以专注于复杂的领域模型,它是模型的一部分??,而另一个经验不足的团队可以专注于模型和用户界面
?场景中,预计随着时间的推移,系统,并且可以包含多个版本的模型或者业务规则经常改变
?与其他系统,特别是与事件采购,其中一个子系统的瞬时故障不会影响到其它的可用性的组合一体化。

这种模式可能不适合下列情况
?域或业务规则很简单
?一个简单的CRUD风格的用户界面和相关的数据访问操作就足够了。
?对于在整个系统中的实现一个整体的数据管理方案,其中CQRS可以是有用的特定组件但是它可以增加实际上并不需要相当大的往往是不必要的复杂性。 


事件获取CQRS


CQRS模式常用于事件获取图案一起使用。CQRS为基础的系统使用分离的读取和写入的数据模型每个针对有关任务通常位于物理上分离的存储区。当与采购活动时,事件的存储是模式,这是信息的权威来源。一个CQRS为基础的系统的读取模型提供数据的物化视图通常是高度非规范化的意见这些视图量身定做的接口和应用程序,这有助于最大程度地显示和查询性能显示要求。

使用事件作为写入存储区而不是实际的数据,在一个时间点避免了在单个聚合更新冲突并最大限度地提高性能可扩展性。事件可用于异步生成用于填充读取存储器中的数据实体化视图

由于事件存储是信息的权威来源就可以删除物化视图和回放所有过去的事件来创建当前状态的一个新表示系统升级时,或者当读取模式必须改变物化视图有效的数据的耐用只读缓存

当使用CQRS结合事件获取模式考虑以下几点:
?与任何系统,其中写入和读出存储是分开的,在此基础上图案系统唯一最终一致。将有生成的事件数据存储器保存由这些事件被更新启动操作的结果之间有一些延迟
?模式引入由于代码必须创建启动处理事件并组装或者更新查询或读取模型所需的适当的意见或物体额外的复杂性采购活动一起使用CQRS模式固有的复杂性时,可以做一个成功的实现更加困难,需要重新学习的一些概念和不同的方法来设计系统。然而事件采购可以更容易地进行建模并且可以更容易地重建的观点或创建新的,因为变化数据的意图将被保留。
?生成物化视图读取模型数据通过重放和处理特定的实体或实体的集合的事件突起的使用可能需要相当多的处理时间资源的使用,尤其是如果它需要求和或值的数据在长时间内的,因为所有的相关联的事件可能需要被审查。这可以通过实现数据快照预定的时间间隔已经发生的特定操作的次数,一个实体的当前状态的总计数被部分地解决。



注意:

欲了解更多信息,请参阅活动采购模式物化视图模式以及模式与实践指导CQRS之旅MSDN上尤其是你应该阅读的章节介绍采购活动进行全面的探索模式,以及它如何CQRS有用的CQRS和ES深潜了解更多包括如何聚集分区可以在微软的AzureCQRS使用


例子


下面的代码显示了一个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命令执行此操作。
<span></span><pre class="csharp" name="code">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环境中,这些方法将有通用名称,如保存或更新并有一个DTO作为唯一的参数CQRS方法可以更好地定制,以满足该组织开展业务及库存管理的方式。

本文翻译自MSDN:http://msdn.microsoft.com/en-us/library/dn568103.aspx

云计算设计模式(六)——命令和查询职责分离(CQRS)模式