CQRS读写职责分离模式(Command and Query Responsibility Segregation (CQRS) Pattern)

此文翻译自msdn,侵删。

原文地址:https://msdn.microsoft.com/en-us/library/dn568103.aspx

通过使用不同的接口来分离读和写操作,这种模式最大化了系统的性能,伸缩性和安全性;能够提供更大的灵活性以支持系统的扩展升级;并且能够防止领域内的更新操作造成的冲突。

实际情况和问题

在传统的数据管理系统中,commands (更新数据)和queries (查询数据)都依赖仓储(repository)中的一些相同实体。在传统的数据库,例如SQL Server中,这些实体可能只是一张或多张数据表中的一部分。

特别的,在这些系统中,所有的增删改查(CRUD)操作都依赖相同的实体。例如,一个用户从数据获取层(DAL)获得一个包含一个消费者信息的数据传输对象(DTO),然后展示到屏幕上。这个用户更新了这个DTO的某些字段,然后将这个DTO通过DAL层保存起来。相同的DTO被用来进行读和写操作,如图1所示。

IC702503

图1-一个传统的CRUD架构

传统的CRUD设计在数据操作的业务逻辑比较简单的时候能够工作得很好。一些开发工具提供的代码生成功能(Scaffold mechanisms )能够很快地生成数据类,然后可以根据需要修改这些代码。

然而,这种传统的CRUD方式有一些缺点:

  • 这种机制常常意味着数据的读和写之间有一些不对应,例如在某些操作中尽管有一些额外的属性并不需要,他们还是得被正确地更新。
  • 在一些比较精密的领域中可能会遭遇一些数据冲突(多人同时操作同一组数据的时候),或者由于使用乐观锁造而成更新冲突。这些问题都会随着整个系统的复杂度和吞吐量的提高而增加。此外,由于需要从数据库和数据获取层执行一些复杂的查询,传统的方法对对性能还会造成一些负面的影响。
  • 这种机制会使安全和权限管理更加麻烦,因为每个实体都受到读写操作的影响,所以在一些情况下会不小心暴露一些不必要出现的信息。

解决方案

读写职责分离模式(CQRS)是一种把查询(Queries) 数据和和更新(Commands) 数据通过使用各自独立的接口分开的模式。这就意味着用来查询和更新的数据模型是不一样的。这些模型可以被各自分离独立,就像图2展示的那样,尽管这个要求不是一个绝对的。

IC702503 (1)

图2 - 一个基本的CQRS架构

对比于传统的基于CRUD模式的单独数据模型,使用基于CQRS的读写分离模型明显地简化了设计和实现。然而,读写分离模式有一个缺点,那就是不能使用代码生成器自动生成代码。

用于读的模型和用于写的模型可能都来自于同一个数据源,也许是使用SQL视图,也可能是简单的数据映射。无论怎么样,一种常见的做法是将读写数据分离储存在不同的物理存储系统中来使系统的性能,伸缩性和安全性最大化,就像图3展示的那样。

IC702504

图3 - 一个使用分离读和写数据库的CQRS架构

用于读的数据库可以是一个用于写的数据库的简单复制,它是只读的。或者读和写数据库采用各自的结构。使用只读的“读”数据库集群可以有效地提升读取性能和用户界面的响应速度,尤其是在分布式场景中。一些数据库系统,比如SQL Server,额外地提供了一些机制,比如故障切换,来支持这种模式。

分离读写数据库同时可以实现数各自的负载均衡,例如,“读”的数据库要比“写”的数据库承担更多的负载。

如果一个读写模型包含一些格式化的信息(参见物化视图模式),在从视图中获取数据或者从系统中获取数据的时候,性能就能够得到保证。

更多关于CQRS模式的信息和它的实现,可以参考以下资源:

一些问题和考虑

当考虑如何实现这个模式的时候,需要考虑以下几点:

  • 将读写数据库分开可以提升性能和系统的安全性,但是这种做法会增加回溯(resiliency)和最终一致性(eventual consistency)的复杂度。写模型的数据库一旦更新,读模型的数据库必须响应更新,并且可能很难发现用户是使用旧的读数据进行更新——意味着这种操作无法完成。(The read model store must be updated to reflect changes to the write model store, and it may be difficult to detect when a user has issued a request based on stale read data—meaning that the operation cannot be completed.)
  • 考虑将CQRS应用在你系统中最有价值的部分,并且从中汲取经验。
  • 一个支持最终一致性的方式是结合使用事件溯源和CQRS模式,因此这个写模式中的操作可以被当作一组只增的更新实体的事件流。这些事件可以被用来更新成物化视图当作读模型使用。可以通过阅读Event Sourcing and CQRS来获取更多信息。

什么时候使用这种模式

这种模式在以下场景中适用:

  • 存在对同一数据进行多种操作的精密领域。CQRS能够让你在足够的粒度上面去定义存储,在领域层面减少更新数据造成的冲突(or any conflicts that do arise can be merged by the command)尽管是在更新相同类型的数据的时候。
  • 使用基于任务的用户界面(用户被一系列的引导步骤指引操作)与复杂的领域模型,并且团队对领域设计模式已经很熟悉。
  • 数据读取必须
  • 一部分的开发人员可以专注于业务领域,另一部分经验相对不丰富的开发者可以专注于读模型和用户界面。
  • 系统会更新并且存在不同的模型版本,或者业务逻辑有规律地变动。
  • 和其他系统结合,特别是事件溯源系统,并且子系统暂时性的故障不会影响到其他系统。

这种模式在下面几个场景中不适用:

  • 领域和业务规则很简单。
  • 简单的基于CRUD的用户界面和相关的数据操作就足够了。
  • 对于整个系统的实现,有一个可以应用CQRS的数据管理业务场景,但是却会增加很多不必要的复杂度,而这个复杂度是不能被接受的。

This pattern is ideally suited to:

  • Collaborative domains where multiple operations are performed in parallel on the same data. CQRS allows you to define commands with a sufficient granularity to minimize merge conflicts at the domain level (or any conflicts that do arise can be merged by the command), even when updating what appears to be the same type of data.
  • Use with task-based user interfaces (where users are guided through a complex process as a series of steps), with complex domain models, and for teams already familiar with domain-driven design (DDD) techniques. The write model has a full command-processing stack with business logic, input validation, and business validation to ensure that everything is always consistent for each of the aggregates (each cluster of associated objects that are treated as a unit for the purpose of data changes) in the write model. The read model has no business logic or validation stack and just returns a DTO for use in a view model. The read model is eventually consistent with the write model.
  • Scenarios where performance of data reads must be fine-tuned separately from performance of data writes, especially when the read/write ratio is very high, and when horizontal scaling is required. For example, in many systems the number of read operations is orders of magnitude greater that the number of write operations. To accommodate this, consider scaling out the read model, but running the write model on only one or a few instances. A small number of write model instances also helps to minimize the occurrence of merge conflicts.
  • Scenarios where one team of developers can focus on the complex domain model that is part of the write model, and another less experienced team can focus on the read model and the user interfaces.
  • Scenarios where the system is expected to evolve over time and may contain multiple versions of the model, or where business rules change regularly.
  • Integration with other systems, especially in combination with Event Sourcing, where the temporal failure of one subsystem should not affect the availability of the others.

This pattern might not be suitable in the following situations:

  • Where the domain or the business rules are simple.
  • Where a simple CRUD-style user interface and the related data access operations are sufficient.
  • For implementation across the whole system. There are specific components of an overall data management scenario where CQRS can be useful, but it can add considerable and often unnecessary complexity where it is not actually required.

事件溯源和CQRS

CQRS经常和事件溯源模式结合使用。基于CQRS的系统使用分离的读和写模型,每一个都对应相应的任务并且一般储存在不同的数据库中。当和事件溯源模式一起使用的时候,一系列的事件存储相当于“写”模型,是所有信息的可信赖来源(authoritative source )。基于CQRS的系统的读模型提供了数据的物化视图,经常是一种高度格式化的视图形式。这些视图对应相应的界面并且展示了应用程序的需求,帮助最大化展示和查询效率。

使用一系列的事件当作“写”而不是某一个时间点的数据,避免了更新的冲突并且最大化性能和系统的伸缩性,这些事件可以被异步地产生被用来展示数据的物化视图。

因为事件数据库是所有信息的可信赖来源,当系统改进的时候,有可能删除物化视图并且展示所有过去的时间来产生一个新的数据,或者当读模型必须改变的时候。物化视图是一个长久的数据缓存。

当将CQRS和事件溯源模式结合起来的时候,考虑以下几点:

  • 对于任何的读写分离储存的系统,这些系统基于事件溯源模式都是“最终一致”的。因此在事件产生和数据存储之间会有一些延迟。
  • 这种模式会造成一些额外的复杂度,因为代码必须要能够初始化和处理事件,然后组合或者更新相应的读写模型需要视图或者对象。这种复杂度会对让系统的实现变得有些困难,需要重新学习一些概念和一个不同的设计系统的方式。然而事件溯源可以让为领域建模,让重建视图或者对象更加容易。
  • 生成物化视图

The CQRS pattern is often used in conjunction with the Event Sourcing pattern. CQRS-based systems use separate read and write data models, each tailored to relevant tasks and often located in physically separate stores. When used with Event Sourcing, the store of events is the write model, and this is the authoritative source of information. The read model of a CQRS-based system provides materialized views of the data, typically as highly denormalized views. These views are tailored to the interfaces and display requirements of the application, which helps to maximize both display and query performance.

Using the stream of events as the write store, rather than the actual data at a point in time, avoids update conflicts on a single aggregate and maximizes performance and scalability. The events can be used to asynchronously generate materialized views of the data that are used to populate the read store.

Because the event store is the authoritative source of information, it is possible to delete the materialized views and replay all past events to create a new representation of the current state when the system evolves, or when the read model must change. The materialized views are effectively a durable read-only cache of the data.

When using CQRS combined with the Event Sourcing pattern, consider the following:

  • As with any system where the write and read stores are separate, systems based on this pattern are only eventually consistent. There will be some delay between the event being generated and the data store that holds the results of operations initiated by these events being updated.
  • The pattern introduces additional complexity because code must be created to initiate and handle events, and assemble or update the appropriate views or objects required by queries or a read model. The inherent complexity of the CQRS pattern when used in conjunction with Event Sourcing can make a successful implementation more difficult, and requires relearning of some concepts and a different approach to designing systems. However, Event Sourcing can make it easier to model the domain, and makes it easier to rebuild views or create new ones because the intent of the changes in the data is preserved.
  • Generating materialized views for use in the read model or projections of the data by replaying and handling the events for specific entities or collections of entities may require considerable processing time and resource usage, especially if it requires summation or analysis of values over long time periods, because all the associated events may need to be examined. This may be partially resolved by implementing snapshots of the data at scheduled intervals, such as a total count of the number of a specific action that have occurred, or the current state of an entity.

例子

下面的代码展示了从一个CQRS实现中截取的部分代码,这段代码使用不同的读写模型。模型的接口并没有表名任何对应数存储的特性,并且他们可以被改进和独立地细致调整因为他们的接口是分开的。

下面的代码展示了读模型的定义:

 1 // Query interface
 2 namespace ReadModel
 3 {
 4   public interface ProductsDao
 5   {
 6     ProductDisplay FindById(int productId);
 7     IEnumerable<ProductDisplay> FindByName(string name);
 8     IEnumerable<ProductInventory> FindOutOfStockProducts();
 9     IEnumerable<ProductDisplay> FindRelatedProducts(int productId);
10   }
11 
12   public class ProductDisplay
13   {
14     public int ID { get; set; }
15     public string Name { get; set; }
16     public string Description { get; set; }
17     public decimal UnitPrice { get; set; }
18     public bool IsOutOfStock { get; set; }
19     public double UserRating { get; set; }
20   }
21 
22   public class ProductInventory
23   {
24     public int ID { get; set; }
25     public string Name { get; set; }
26     public int CurrentStock { get; set; }
27   }
28 }

系统允许用户给商品打分,代码通过使用RateProduct方法来实现

 1 public interface Icommand
 2 {
 3   Guid Id { get; }
 4 }
 5 
 6 public class RateProduct : Icommand
 7 {
 8   public RateProduct()
 9   {
10     this.Id = Guid.NewGuid();
11   }
12   public Guid Id { get; set; }
13   public int ProductId { get; set; }
14   public int rating { get; set; }
15   public int UserId {get; set; }
16 }

系统使用ProductsCommandHandler类去处理应用程序发送的“写”动作。用户通过通讯模块常以队列的形式向领域发送“写”的请求。处理句柄就接受请求然后调用领域的接口方法。每一个请求的粒度都可以被精心设计以缓解冲突。下面的代码展示了一个大致的ProductsCommandHandler类大致的样子。

 1 public class ProductsCommandHandler : 
 2     ICommandHandler<AddNewProduct>,
 3     ICommandHandler<RateProduct>,
 4     ICommandHandler<AddToInventory>,
 5     ICommandHandler<ConfirmItemShipped>,
 6     ICommandHandler<UpdateStockFromInventoryRecount>    
 7 {
 8   private readonly IRepository<Product> repository;
 9 
10   public ProductsCommandHandler (IRepository<Product> repository)
11   {
12     this.repository = repository;
13   }
14 
15   void Handle (AddNewProduct command)
16   {
17     ...
18   }
19 
20   void Handle (RateProduct command)
21   {
22     var product = repository.Find(command.ProductId);
23     if (product != null)
24     {
25       product.RateProuct(command.UserId, command.rating);
26       repository.Save(product);
27     }
28   }
29 
30   void Handle (AddToInventory command)
31   {
32     ...
33   }
34 
35   void Handle (ConfirmItemsShipped command)
36   {
37     ...
38   }
39 
40   void Handle (UpdateStockFromInventoryRecount command)
41   {
42     ...
43   }
44 }

下面的代码展示了“写“模型的ProductsDomain 的接口。

1 public interface ProductsDomain
2 {
3   void AddNewProduct(int id, string name, string description, decimal price);
4   void RateProduct(int userId int rating);
5   void AddToInventory(int productId, int quantity);
6   void ConfirmItemsShipped(int productId, int quantity);
7   void UpdateStockFromInventoryRecount(int productId, int updatedQuantity);
8 }

同样需要注意的是ProductsDomain接口包含的方法对领域是有意义的。传统的CRUD环境中这些方法的名字都只是的类似于”save“或者”update“这样泛泛的名字,然后有一个DTO作为唯一的参数。比起传统的CRUD模式,CQRS方法可以更好地适应这样一个业务和存货管理系统。

相关的模式和指南

在实现这种模式的时候,以下的模式和指南或许也与之相关:

  • 数据一致基础 这个指南解释了在由于在使用CQRS模式情况下会遇见的读写最终一致问题,并且告诉我们这些问题如何解决。
  • 数据分表指南 这个指南描述了在使用CQRS模式时候如何分表存储读和写的数据去提高系统的伸缩性,减少竞争冲突和提高性能。
  • 事件溯源模式 这个模式更详细地介绍了事件溯源如何与CQRS模式结合以简化复杂领域中的任务;提高性能,可伸缩性和响应性;为事务性数据提供一致性基础;并且保证了完整的审计途径和历史以满足修正操作需要。
  • 物化视图模式 CQRS的读模型的实现可能包含写模型的物化视图。读模型也可能被用来生成物化视图。
原文地址:https://www.cnblogs.com/balavatasky/p/6085814.html