云计算设计模式(六)——命令和查询职责分离(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深潜了解更多,包括如何聚集分区可以在微软的Azure 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命令执行此操作。
- <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方法可以更好地定制,以满足该组织开展业务及库存管理的方式。