前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >命令和查询责任隔离(CQRS)模式

命令和查询责任隔离(CQRS)模式

作者头像
35岁程序员那些事
发布2020-02-24 13:03:03
1K0
发布2020-02-24 13:03:03
举报
文章被收录于专栏:35岁程序员那些事

命令和查询责任隔离(CQRS)模式

通过使用单独的接口将读取数据的操作与更新数据的操作隔离开来。这可以最大化性能、可伸缩性和安全性。通过更高的灵活性支持系统随时间的发展,并防止更新命令在域级别引起合并冲突。

背景和问题

在传统的体系结构中,使用相同的数据模型来查询和更新数据库。这很简单,适用于基本的CRUD操作。然而,在更复杂的应用程序中,这种方法可能变得笨拙。例如,在读取端,应用程序可能执行许多不同的查询,返回具有不同形状的数据传输对象(dto)。对象映射可能变得复杂。在写端,模型可能实现复杂的验证和业务逻辑。结果,您可能会得到一个做得太多的过于复杂的模型。

读和写工作负载通常是不对称的,性能和规模需求非常不同。

  • 数据的读和写表示形式之间常常存在不匹配,比如必须正确更新的附加列或属性,尽管它们不是操作的一部分。
  • 当对同一组数据并行执行操作时,可能会发生数据争用。
  • 由于数据存储和数据访问层的负载以及检索信息所需的查询的复杂性,传统方法可能会对性能产生负面影响。
  • 管理安全性和权限可能变得复杂,因为每个实体都受读和写操作的约束,这可能会在错误的上下文中公开数据。

解决方案

CQRS地址将读写分离到单独的模型中,使用命令来更新数据,使用查询来读取数据。

  • 命令应该基于任务,而不是以数据为中心。(“预订酒店房间”,而不是“将预订状态设置为reservation .”)命令可以放在队列中进行异步处理,而不是同步处理。
  • 查询从不修改数据库。查询返回不封装任何域知识的DTO。

然后可以隔离模型,如下图所示,尽管这不是绝对的要求。

拥有独立的查询和更新模型可以简化设计和实现。然而,一个缺点是CQRS代码不能使用诸如O/RM工具之类的脚手架机制从数据库模式自动生成。

为了更好地隔离,您可以物理地将读数据与写数据分离。在这种情况下,read数据库可以使用为查询优化的自己的数据模式。例如,它可以存储数据的物化视图,以避免复杂的连接或复杂的O/RM映射。它甚至可能使用不同类型的数据存储。例如,写数据库可能是关系数据库,而读数据库是文档数据库。

如果使用单独的读和写数据库,它们必须保持同步。通常,这是通过让写模型在更新数据库时发布事件来实现的。更新数据库和发布事件必须在单个事务中进行。

读存储可以是写存储的只读副本,或者读和写存储可以具有完全不同的结构。使用多个只读副本可以提高查询性能,特别是在分布式场景中,只读副本位于应用程序实例附近。

读写存储的分离还允许对每个存储进行适当的缩放,以匹配负载。例如,读存储通常会遇到比写存储高得多的负载。

CQRS的一些实现使用事件源模式。使用此模式,应用程序状态存储为一系列事件。每个事件表示对数据的一组更改。当前状态由重播事件构造。在CQRS上下文中,事件源的一个好处是可以使用相同的事件通知其他组件——特别是通知读模型。read模型使用事件创建当前状态的快照,这对于查询更有效。然而,事件源增加了设计的复杂性。

CQRS的好处包括:

  • 独立的扩展。CQRS允许读写工作负载独立伸缩,并且可能导致更少的锁争用。
  • 优化的数据模式。读端可以使用为查询优化的模式,而写端使用为更新优化的模式。
  • 安全。更容易确保只有正确的域实体才对数据执行写操作。
  • 关注点分离。分离读和写端可以得到更易于维护和灵活的模型。大多数复杂的业务逻辑都进入了写模型。读取模型可以相对简单。
  • 简单的查询。通过在read数据库中存储物化视图,应用程序可以在查询时避免复杂的连接。

问题和注意事项

实施这一模式的一些挑战包括:

  • 复杂性。CQRS的基本思想很简单。但这可能导致更复杂的应用程序设计,特别是如果它们包含事件源模式。
  • 消息传递性。虽然CQRS不需要消息传递,但是通常使用消息传递来处理命令和发布更新事件。在这种情况下,应用程序必须处理消息失败或重复消息。
  • 最终一致性。如果将读和写数据库分开,则读数据可能会过时。必须更新读模型存储以反映对写模型存储的更改,并且很难检测用户何时基于陈旧的读数据发出了请求。

何时使用此模式

在以下情况下考虑CQRS:

  • 许多用户并行访问相同数据的协作域。CQRS允许您定义具有足够粒度的命令,以最小化域级别上的合并冲突,并且确实出现的冲突可以通过该命令进行合并。
  • 基于任务的用户界面,在此界面中,用户通过一系列步骤或使用复杂的域模型完成复杂的过程。写模型有一个完整的命令处理堆栈,其中包含业务逻辑、输入验证和业务验证。写模型可以将一组关联对象视为数据更改的单个单元(DDD术语中的聚合),并确保这些对象始终处于一致的状态。读取模型没有业务逻辑或验证堆栈,只返回一个DTO以便在视图模型中使用。读模型最终与写模型保持一致。
  • 必须将数据读取的性能与数据写入的性能分开调优,特别是当读取的数量远远大于写入的数量时。在这个场景中,您可以扩展读模型,但是只在几个实例上运行写模型。少量的写模型实例也有助于最小化合并冲突的发生。
  • 场景中,一个开发团队可以专注于作为写模型一部分的复杂领域模型,而另一个团队可以专注于读模型和用户界面。
  • 预期系统将随时间发展的场景,可能包含模型的多个版本,或者业务规则定期更改的场景。
  • 与其他系统的集成,特别是与事件源的结合,其中一个子系统的时间故障不应该影响其他子系统的可用性。

这种模式不推荐在什么时候使用当:

  • 域或业务规则很简单。
  • 一个简单的crud风格的用户界面和数据访问操作就足够了。

考虑将CQRS应用于系统中最有价值的有限部分。

事件溯源和CQRS

CQRS模式通常与事件源模式一起使用。基于cqrs的系统使用独立的读和写数据模型,每个模型都根据相关任务进行定制,通常位于物理上独立的存储中。当与事件源模式一起使用时,事件的存储是写模型,并且是正式的信息源。基于cqrs的系统的读取模型提供数据的物化视图,通常为高度非规范化视图。这些视图是根据应用程序的接口和显示需求定制的,这有助于最大化显示和查询性能。

使用事件流作为写存储,而不是在某个时间点使用实际数据,可以避免单个聚合上的更新冲突,并最大化性能和可伸缩性。事件可用于异步生成用于填充读取存储的数据的物化视图。

因为事件存储是正式的信息源,所以可以删除物化视图并重播所有过去的事件,从而在系统发展时或读取模型必须更改时创建当前状态的新表示。物化视图实际上是数据的持久只读缓存。

当使用CQRS与事件源模式结合使用时,请考虑以下因素:

  • 与任何读写存储独立的系统一样,基于此模式的系统最终只能保持一致。在生成事件和更新数据存储之间会有一些延迟。
  • 模式增加了复杂性,因为必须创建代码来发起和处理事件,并组装或更新查询或读取模型所需的适当视图或对象。当与事件源模式一起使用时,CQRS模式的复杂性会使成功的实现变得更加困难,并且需要使用不同的方法来设计系统。然而,事件源可以使对域建模变得更容易,并使重构视图或创建新视图变得更容易,因为数据中更改的意图得到了保留。
  • 通过对特定实体或实体集合的事件进行重播和处理,为数据的读取模型或投影生成物化视图可能需要大量的处理时间和资源使用。如果需要长时间对值进行求和或分析,尤其如此,因为可能需要检查所有相关的事件。解决这个问题的方法是按预定的时间间隔实现数据快照,例如已发生的特定操作的总数或实体的当前状态。

举例

下面的代码显示了CQRS实现示例的一些摘录,该实现对读和写模型使用了不同的定义。模型接口不指定底层数据存储的任何特性,而且由于这些接口是分离的,它们可以独立地发展和微调。

下面的代码显示了读取的模型定义。

代码语言:javascript
复制
// 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来实现这一点。

代码语言:javascript
复制
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类的大纲。

代码语言:javascript
复制
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)
  {
    ...
  }
}
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-06-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 架构随笔录 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 命令和查询责任隔离(CQRS)模式
    • 背景和问题
      • 解决方案
        • 问题和注意事项
          • 何时使用此模式
            • 事件溯源和CQRS
              • 举例
              相关产品与服务
              对象存储
              对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档