通过使用单独的接口将读取数据的操作与更新数据的操作隔离开来。这可以最大化性能、可伸缩性和安全性。通过更高的灵活性支持系统随时间的发展,并防止更新命令在域级别引起合并冲突。
在传统的体系结构中,使用相同的数据模型来查询和更新数据库。这很简单,适用于基本的CRUD操作。然而,在更复杂的应用程序中,这种方法可能变得笨拙。例如,在读取端,应用程序可能执行许多不同的查询,返回具有不同形状的数据传输对象(dto)。对象映射可能变得复杂。在写端,模型可能实现复杂的验证和业务逻辑。结果,您可能会得到一个做得太多的过于复杂的模型。
读和写工作负载通常是不对称的,性能和规模需求非常不同。
CQRS地址将读写分离到单独的模型中,使用命令来更新数据,使用查询来读取数据。
然后可以隔离模型,如下图所示,尽管这不是绝对的要求。
拥有独立的查询和更新模型可以简化设计和实现。然而,一个缺点是CQRS代码不能使用诸如O/RM工具之类的脚手架机制从数据库模式自动生成。
为了更好地隔离,您可以物理地将读数据与写数据分离。在这种情况下,read数据库可以使用为查询优化的自己的数据模式。例如,它可以存储数据的物化视图,以避免复杂的连接或复杂的O/RM映射。它甚至可能使用不同类型的数据存储。例如,写数据库可能是关系数据库,而读数据库是文档数据库。
如果使用单独的读和写数据库,它们必须保持同步。通常,这是通过让写模型在更新数据库时发布事件来实现的。更新数据库和发布事件必须在单个事务中进行。
读存储可以是写存储的只读副本,或者读和写存储可以具有完全不同的结构。使用多个只读副本可以提高查询性能,特别是在分布式场景中,只读副本位于应用程序实例附近。
读写存储的分离还允许对每个存储进行适当的缩放,以匹配负载。例如,读存储通常会遇到比写存储高得多的负载。
CQRS的一些实现使用事件源模式。使用此模式,应用程序状态存储为一系列事件。每个事件表示对数据的一组更改。当前状态由重播事件构造。在CQRS上下文中,事件源的一个好处是可以使用相同的事件通知其他组件——特别是通知读模型。read模型使用事件创建当前状态的快照,这对于查询更有效。然而,事件源增加了设计的复杂性。
CQRS的好处包括:
实施这一模式的一些挑战包括:
在以下情况下考虑CQRS:
这种模式不推荐在什么时候使用当:
考虑将CQRS应用于系统中最有价值的有限部分。
CQRS模式通常与事件源模式一起使用。基于cqrs的系统使用独立的读和写数据模型,每个模型都根据相关任务进行定制,通常位于物理上独立的存储中。当与事件源模式一起使用时,事件的存储是写模型,并且是正式的信息源。基于cqrs的系统的读取模型提供数据的物化视图,通常为高度非规范化视图。这些视图是根据应用程序的接口和显示需求定制的,这有助于最大化显示和查询性能。
使用事件流作为写存储,而不是在某个时间点使用实际数据,可以避免单个聚合上的更新冲突,并最大化性能和可伸缩性。事件可用于异步生成用于填充读取存储的数据的物化视图。
因为事件存储是正式的信息源,所以可以删除物化视图并重播所有过去的事件,从而在系统发展时或读取模型必须更改时创建当前状态的新表示。物化视图实际上是数据的持久只读缓存。
当使用CQRS与事件源模式结合使用时,请考虑以下因素:
下面的代码显示了CQRS实现示例的一些摘录,该实现对读和写模型使用了不同的定义。模型接口不指定底层数据存储的任何特性,而且由于这些接口是分离的,它们可以独立地发展和微调。
下面的代码显示了读取的模型定义。
// Query interface
namespace ReadModel
{
public interface ProductsDao
{
ProductDisplay FindById(int productId);
ICollection<ProductDisplay> FindByName(string name);
ICollection<ProductInventory> FindOutOfStockProducts();
ICollection<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; }
}
}
该系统允许用户对产品进行评分。应用程序代码使用下面代码中显示的RateProductcommand来实现这一点。
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.RateProduct(command.UserId, command.Rating);
repository.Save(product);
}
}
void Handle (AddToInventory command)
{
...
}
void Handle (ConfirmItemsShipped command)
{
...
}
void Handle (UpdateStockFromInventoryRecount command)
{
...
}
}