CQRS

  • 命令查询职责分离模式[Command Query Responsibility Segregation].
  • 从业务上分离修改 (Command,增,删,改,会对系统状态进行修改)和查询(Query,查,不会对系统状态进行修改)的行为,即常说的读写隔离。

什么是CQS

基本思想在于,任何一个对象的方法可以分为两大类:

  • 命令(Command):不返回任何结果(void),但会改变对象的状态。
  • 查询(Query):返回结果,但是不会改变对象的状态,对系统没有副作用。
private int i = 0;

// 原来的设计
private int Increase(int value)
{
    i += value;
    return i;
}

// 分离后的设计
private void IncreaseCommand(int value)
{
    i += value;
}
private int QueryValue()
{
    return i;
}

CQRS是对CQS模式的进一步改进成的一种简单模式:

  • CQRS只是简单的将之前只需要创建一个对象拆分成了两个对象,这种分离基于方法是执行命令还是执行查询这一原则来定的(这个和CQS的定义一致)。

CQRS使用分离的接口将数据查询操作(Queries)和数据修改操作(Commands)分离开来,这也意味着在查询和更新过程中使用的数据模型也是不一样的,进而将读和写逻辑隔离开来。

CQRS模式优点和适用场景

优点

  • 不同的部分分工明确
  • 将业务上的命令和查询的职责分离能够提高系统的性能、可扩展性和安全性。并且在系统的演化中能够保持高度的灵活性,能够防止出现CRUD模式中,对查询或者修改中的某一方进行改动,导致另一方出现问题的情况。
  • 逻辑清晰,能够看到系统中的那些行为或者操作导致了系统的状态变化。
  • 可以从数据驱动(Data-Driven) 转到任务驱动(Task-Driven)以及事件驱动(Event-Driven)。【可以参考事件溯源(Event Sourcing)

适用场景

  • 当在业务逻辑层有很多操作需要相同的实体或者对象进行操作的时候。CQRS使得我们可以对读和写定义不同的实体和方法,从而可以减少或者避免对某一方面的更改造成冲突。
  • 对于一些基于任务的用户交互系统,通常这类系统会引导用户通过一系列复杂的步骤和操作,通常会需要一些复杂的领域模型,并且整个团队已经熟悉领域驱动设计技术。写模型有很多和业务逻辑相关的命令操作的堆,输入验证,业务逻辑验证来保证数据的一致性。读模型没有业务逻辑以及验证堆,仅仅是返回DTO对象为视图模型提供数据。读模型最终和写模型相一致。
  • 适用于一些需要对查询性能和写入性能分开进行优化的系统,尤其是读/写比非常高的系统,横向扩展是必须的。比如,在很多系统中读操作的请求远大于写操作。为适应这种场景,可以考虑将写模型抽离出来单独扩展,而将写模型运行在一个或者少数几个实例上。少量的写模型实例能够减少合并冲突发生的情况。
  • 适用于一些团队中,一些有经验的开发者可以关注复杂的领域模型,这些用到写操作,而另一些经验较少的开发者可以关注用户界面上的读模型。
  • 对于系统在将来会随着时间不段演化,有可能会包含不同版本的模型,或者业务规则经常变化的系统。
  • 需要和其他系统整合,特别是需要和事件溯源(Event Sourcing)进行整合的系统,这样子系统的临时异常不会影响整个系统的其他部分。

不适用场景

  • 领域模型或者业务逻辑比较简单,这种情况下使用CQRS会把系统搞复杂。
  • 对于简单的,CRUD模式的用户界面以及与之相关的数据访问操作已经足够的话,没必要使用CQRS,这些都是一个简单的对数据进行增删改查。
  • 不适合在整个系统中到处使用该模式。在整个数据管理场景中的特定模块中CQRS可能比较有用。但是在有些地方使用CQRS会增加系统不必要的复杂性。

CQRS与Event Sourcing的关系

参考Event Sourcing与CQRS

通过CQRS实现避免资源竞争

可采取三种措施:

一个Command总是只修改一个聚合根

  • 缩小事务的范围,确保一个事务一次只涉及一条记录的修改。
  • 通过Saga,我们可以用最终一致性的方式最终实现对多个聚合根的修改。
  • 业务流程中会包括多个参与该流程的聚合根以及一个用于协调聚合根交互的流程管理器(ProcessManager,无状态),流程管理器负责响应流程中的每个聚合根产生的领域事件,然后根据事件发送相应的Command,从而继续驱动其他的聚合根进行操作。
  • 通过事件驱动的方式,来完成整个业务流程。如果流程中的任何一步出现了异常,那我们可以在流程中定义补偿机制实现回退操作。

对修改同一个聚合根的Command进行排队

  • 当有多台服务器在处理Command时,对要修改聚合根的Command根据聚合根的ID进行路由,根据聚合根的ID的hashcode,然后和当前处理Command的服务器数目取模,就能确定当前Command要被路由到哪个服务器上处理。
  • 这样就能确保在服务器数目不变的情况下,针对同一个聚合根实例修改的所有Command都是被路由到同一台服务器处理。然后加上在单个服务器里面内部做的Command排队设计,就能最终保证,对同一个聚合根的修改,同一时刻只有一个线程在进行。

Command和Event的幂等处理

CQRS架构是基于消息驱动的,所以要尽量避免消息的重复消费。否则,可能会导致某个消息被重复消费而导致最终数据无法一致。
对于CQRS架构,主要考虑三个环节的消息幂等处理:

Command的幂等处理

  • 可以通过持久化Command的方式,然后把CommandId作为主键,确保Command不会重复。

Event持久化的幂等处理

针对新增或修改聚合根的Command,总是会产生相应的领域事件(Domain Event)。

  • 可以对每个事件设置一个版本号,即version。【只需确保该版本号唯一】
  • 聚合根本身也有一个版本号,用于记录当前自己的版本是什么,它每次产生下一个事件时,也能根据自己的版本号推导出下一个要产生的事件的版本号是什么。

Event消费时的幂等处理

  • 用一张表,存储某个事件是否被某个Event Handler处理的信息。每次调用Event Handler之前,判断该Event Handler是否已处理过,如果没处理过,就处理,处理完后,插入一条记录到这个表。

results matching ""

    No results matching ""