JoeCao.github.io icon indicating copy to clipboard operation
JoeCao.github.io copied to clipboard

CQRS初探

Open JoeCao opened this issue 6 years ago • 0 comments

title: CQRS初探

date: 2017-08-26


CQRS初探

背景

我们惯常开发的概念中,一直以CRUD为主(Create、Retrieve、Update、Delete),默认我们读操作(Retrieve)的对象模型和写操作(Create、Update、Delete)是同一个对象、同一张表,这对简单的业务逻辑来说,比较合适,但是在复杂的业务逻辑下,CRUD就有点力不从心了。

AD05C010-B74F-4C8C-AF51-E1BAF16541EE

Command(写)操作

我们对写操作的定义是会改变对象状态的一种操作,状态指的是广义上的,包含了对象所有属性的修改的一种操作,增删改属于写操作。 在我熟悉的电商业务中,写逻辑是比较复杂的,在写之前需要校验很多状态,拿常见的订单创建场景来说,我们需要检查库存的情况,校验用户价格是否正确,校验匹配的优惠活动,校验用户的积分使用情况..... 在退货的场景下面,需要检查双方达成的退款协议,检验货品是否已经处于发货路上,上述任意条件不达成,就不能退货,然后还要有退还积分、优惠劵等一系列的写操作。

从上面的例子可以看出,完成Command主要有三种情况

  • 校验,一系列业务逻辑校验
  • 变更,变更领域对象中的属性
  • 派生,通知相关领域的变更

业务越重,写操作的逻辑复杂性越高,我们的领域驱动设计中的充血模型为什么要强调将数据和操作统一在一个对象中,就是为了解决写操作的复杂逻辑不要散布到其他地方,在领域模型中完成 — 高内聚。

Query(读)操作

读操作就是查询数据返回,相比写操作,比较简单一些。我们往往认为相对Command操作,Query没啥业务逻辑,就是把数据展示出来好了,这里并不涉及领域模型的事情。 不过我们的产品经理常常在数据读取显示上给程序员出难题, 比如

  • 在采购商品列表界面要显示这个商品上一次的采购价格和平均采购价格;
  • 要求显示用户的同时显示最新的一条朋友圈状态等等
  • 要求客户经理查询订单时,只能看到自己名下客户的订单,不能看到全部的订单

这里面就涉及到了一系列表的join查询,甚至sum、avg等等函数运算,但是请注意这里的查询已经涉及到了业务逻辑,查询程序需要了解领域对象之间的关联原则,用户的角色包含关系,有权限判断的要求。 这给我们的领域模型带来了不小的困惑,我们的领域模型都是按照写模型设计的,一旦查询涉及业务逻辑甚至多个领域的规则,就会力不从心了。比如只能查询自己名下客户的订单,按照DDD设计订单模型本来没有包含权限的部分,这是两个不同的限界上下文,实现方式只能先查询订单,再到权限模块接口上一个个去验证是否可以显示给界面,这样做不仅效率极低,而且没法实现分页的功能。 于是我们需要尝试新的架构风格,将查询和命令分开,使用不同的模型,也就是CQRS模式。

CQRS(Command Query Responsibility Seperation)

这里我们使用了API网关也就是BFF来区分读、写操作,发送到不同的模块执行,其中对Command Model的修改将通过event和消息队列交给Updater模块去更新查询视图

A34E4BDC-8678-4F45-A4F6-07EECBF43DDF

有个需要注意的地方是,我们的查询指令就不通过任何的服务提供查询接口了,由API网关直接去查询对应的视图数据库。这样做有两个原因

1、 我认为数据库提供的SQL或者ElasticSearch提供的查询DSL已经足够强大了,我们不需要自己再封装一层查询接口,浪费工作量而且用起来很别扭。

2、我希望Query是不含有业务逻辑的,直接查询视图就能得到产品想要的数据,不要在查询中进行逻辑运算。我们应该还是尽量的把逻辑隐藏到领域对象内部。

那么上面说的产品经理的变态查询需求如何解决呢?这里Martin Flower给出了一个思路 Eager Read Derivation

这是一种空间换时间的思路,也就是说,在View Updater的时候,就将内在逻辑连接起来,形成一个物化视图

比如要在采购列表中显示商品的上次采购价格和平均采购价格,这个不在查询的时候完成,而是在商品查询视图中冗余两个价格项(上次价格、平均价格)。 每次采购完成的时候,就去更新这两个项,由消息队列和Updater异步完成,利用了异步消息驱动机制,查询变得简化,对原有领域逻辑没有破坏,看起来是个很完美的解决方案。

要解决权限问题,就在视图上增加权限的标识,甚至为每个不同的角色生成不同的视图。这样困扰我们在权限查询时候的分页问题也可以很好的解决了。

CQRS的缺陷

CQRS在解决复杂业务逻辑的时候很有帮助,但是也有很多问题

  • 因为两个模型使用不同的数据库,所以消耗增加了一倍以上

  • 需要通过消息机制进行数据同步,消息机制的可靠性和及时性对CQRS举足轻重,我们在使用了CQRS后,甚至在新增跳转页面的时候增加了200ms的延时,为的就是避免数据不一致的情况发生。在开发过程中的负担较重,简单的功能变得复杂了。

    所以在简单逻辑的时候,使用CQRS就是多此一举,还是CRUD最方便。

附录:查询命令分离原则 Command-query separation principle

CQS是针对方法的经典oo设计原则,该原则指出,任何方法都可能是如下情况之一;

1- 执行动作(更新,调整。。。。)的命令方法,这种方法通常具有改变对象状态等副作用。并且是void的,没有返回值。

2向调用者返回数据的查询,这种方法没有副作用,不会永久性地改变任何对象的状态。

关键是,一个方法不应该同时属于以上2个类型。

写操作按照严格定义是没有返回值(void),我常常见到那种写操作返回最新对象的方法

User changeName(User user)

这样实际上是将写操作和读操作合在一块了,违反了CQS原则。

版权说明

本文采用 CC BY 3.0 CN协议 进行许可。 可自由转载、引用,但需署名作者且注明文章出处。如转载至微信公众号,请在文末添加作者公众号二维码。

关注我

微信公众号 qrcode_for_8

JoeCao avatar Sep 14 '17 02:09 JoeCao