我的新课
《C2C 电商系统微服务架构120天实战训练营》
在公众号
儒猿技术窝
上线了,感兴趣的同学,可以长按扫描下方二维码了解课程详情:
随着业务不断发展,软件系统的架构也越来越复杂,但无论多复杂的业务最终在系统中实现的时候,无非是读写操作。用户根据业务规则写入商业数据,再根据查询规则获取想要的结果。通常而言我们会讲这些读写的数据放到一个数据库中保存,通过一套模型对其进行读写操作。而在大型系统中往往查询操作远远多于写入操作,于是就有了读写分离的思想,将读操作和写操作的模型分开定义并且提供不同的通道供用户使用。
CQRS(Command-Query Responsibility Segregation)
就是基于这一思想提供的
一种模式
读写分离的模式,今天就围绕着它给大家讲述以下内容:
CQRS(Command-Query Responsibility Segregation) 是一种读写分离的模式,从字面意思上理解Command是命令的意思,其代表写入操作;Query是查询的意思,代表的查询操作,这种模式的主要思想是将数据的写入操作和查询操作分开。
它源于
Bertrand Mayer
设计的命令查询分离(
CQS
)原理。
CQS
声明一个类只能有两种方法:改变状态并返回
void
的方法和返回状态的方法。
而
Greg Young
是负责命名这种模式为
CQRS
并推广它的人。
首先来看看在没有
CQRS
之前是如何处理系统中的修改和查询的吧,
如图
1
所示:
图1
传统的系统请求
传统的系统请求从最左边的
Client
开始,沿着红线往右通过
Application
Service对系统进行请求。这里
Application
Service
可以理解为系统的门面,或者是
Controller
层负责接收客户端的请求,此时请求的内容比较简单基本和数据库中的信息一致,因此这里使用DTO(
Data
Transfer
Object)直接请求。
DTO
经过
Domain
Model
以后直接到达Database,从而沿着蓝色的线条返回给
Client
端。传统的请求方式部分读操作和写操作,都使用同样的数据模型和一套
Domain
Model以及相同的数据库。
从传统操作来看
Client
的请求在经过
Application Service
,用户意图全部被分解为
CRUD
操作,但是在
Domain
Model
中是无法体现的。为保证
DTO
的完整性和一致性,与操作无关的信息会被纳入
DTO
,查询操作和创建操作都共用一个
DTO
,而领域模型的业务流程被弱化。为了适应同时适应查询和创建操作,
DTO
被设计的面面俱到,也就显得臃肿。从而在传输中存在不必要的字段传递。
而且一次操作,在
DTO
与领域对象间进行多次转换,增加了系统复杂度。还有,读写操作将围绕同一数据模型展开,对于读多写少的系统而言效率并不是最高的,特别在读操作为主的高并发系统中缺点就尤为突出。
正因为传统系统架构存在上面这些问题,因此
CQRS
根据读写职责的不同,把领域模型切分为
Command
端与
Query
端两个部分,如图
2
所示,红色线部分就是
Command
端,其对应的是
Domain
Model
对其发送
Command
操作的指令往数据写入状态信息。
Query
端作为查询操作,由蓝色的线表示,通过
Query
Model
向数据库获取信息,通过黑色向左的先返回结果给
Client
。
Command
端与
Query
端都通过
Application
Service
进入系统,共享同一个数据库,但
Command
端只写入状态,
Query
端只读取状态。
图2
CQRS
分为Command
端和 Query端
目前而言已经将读写操作分开了,由于两个操作依旧共用一个数据库,为了提高读写效率数据库的分离就成为必然的选择。如图
3
所示,于是将原来的
Database
,分离为
Writer
Database
和
Reader
Database
分别用于写操作和读操作。为了保证读写操作的数据一致性,需要在两个数据库之间进行数据同步。
由于数据同步是由时效性的,因此写入方是
Command
端,读取方是
Query
端,因此系统智能保证最终一致性。那么如何保证两个库之间的同步呢?下面需要引入
Event
Sourcing
的概念。
Event Sourcing
也叫事件溯源,是
Martin Fowler
提出的一种架构模式。
其设计思想是系统中的业务都由事件驱动来完成。系统中记录的是一个个事件,由这些事件体现信息的状态。业务数据可以是事件产生的视图,不一定要保存到数据库中。
为了便于理解
Event
Sourcing
我们通过一个例子来进一步解释,如图
3
所示:
图3
Command
端和 Query端 读写数据库的分离
我们从左往右看。对于一个业务类“账户”,拥有“属性”包括“账户
ID
”和“账户金额”信息,同时拥有“方法”包括“创建账户”、“存现金”和“取现金”。中间绿色的事件序列,是针对“账户”进行的一些列操作,按照其中的序列号来看。
1.
创建了一个银行账户,假设此时的账户
ID
为“
0
001”
。
2.
针对
“
0
001
”这个账户存入
3
00
元现金。
3.
然后从
“
0
001
”这个账户取出
1
00
元现金。
4.
最后,再存入
2
00
元。
上面
生成的这一系列事件会保存到下方的
Event
Store
的事件库中,这里并不会保存
“
账户
”
的状态
信息。当需要获取
“账户”
数据
的时候,会通过这些事件信息,还原成
“账户”的最终状态,也就是“账户
ID
”为“
0
001
”,“账户金额”为
4
00
。其具体实现方式是,通过账户相关的四个事件对应的处理方法,重新生成当前状态。如果每次查询状态信息都需要这样处理势必会造成资源的浪费,
因此在右侧黄色的部分,我们将最终的
“账户”信息通过视图的方式保存下来,以供查询
。
上面这个
“账户”处理的
过程,就是
Event Sourcing
,
说白了就是通过事件的处理模式。它将系统中的操作都按照事件的方式记录并保存,任何实体的最终状态都是通过事件的叠加和还原确认的。
Event
Sourcing
包含的内容
上面介绍了
Event
Sourcing
的执行原理和基本概念,这里一起来看看其包含的主要内容,便于我们对它有更加全面的理解。
聚合对象:图3的例子中
“
账户
”
就是一个聚合对象,它里面包含
“账户
ID
”、“账户金额”等
的基本信息,也包含了对账户操作
的方法:“创建账户”、“存现金”、“取现金”。同时
“
账户
”
在领域驱动开发中对应的是一个领域模型。
-
Event Store:
在
Event Sourcing
模式中,事件
所保存的数据库称为
Event Store
。在事件中需要包含聚合对象的
ID
,以及事件的顺序。这样在查询的时候可以根据聚合
ID
从数据库中找到相关的事件,并通过事件的序号还原执行顺序。也就是事件的重现,也就是某一时刻执行的事件取出来,调用他的处理函数,还原那个时间点的业务状态。
-
为了获取最新的“账户”状态信息,需要通过
Event
Sourcing
中获取对应的事件进行回放,从而获取当前的状态,这样的操作会浪费很多资源。因此我们会将聚合对象的最新数据状态,写到一个表中,这个表就是视图。又或者将这个状态信息发送给其他的应用程序进行后续的业务操作。
-
查询的内容是针对“账户”最终状态的,因此针对的对象应该是视图。这里的设定刚好的
CQRS
中的读写分离不谋而合,通过
Event
Store存放
Command
端的Event
信息,通过视图存放实体最终状态的信息,而Query
端从视图查询数据返回给用户。
Event Sourcing 的优缺点
上面介绍了
Event
Sourcing
的原理和内容以后再来看看它的优缺点。
-
溯源事件与重现操作:特别是在业务复杂的系统
中,一个事务包含多个操作,它们有的是并行有的串行,如果需要了解操作的执行就需要对每个事件了如指掌。Event
Sourcing
恰恰提供了事件的历史信息,方便查找任何时间点发生的事情。
-
追踪和
修复
Bug
:可以通过事件分析业务的执行过程,
帮助发现Bug,例如重方
Bug
产生时的事件序列,从而定位
Bug
所处位置。
发现
B
ug
并且修复以后,可以通过重新聚合业务数据,
重放执行的事件序列验证修复结果,同时将Bug造成的损失进行挽回。
-
提高性能:
Event Sourcing
模式下,
由于是记录事件执行的序列,因此都是新增操作,没有更新操作,相对于需要更新操作的系统而言记录数据的性能是提高了。如果使用视图的方式将实体的最终状态可以传递给其他的应用,而不用写入数据库以后再读取,这种做法也提高了效率。
-
转变思路:Event
Sourcing的落地需要在设计时就用
领域驱动的方式开展,需要有基于事件的响应式编程思维。这种方式需要以领域模型设计优先,而不是传统的数据库设计优先。
-
变更事件结构:随着业务流程的变化需要不断调整事件结构,对事件添加或者修改一些数据。这种行为会影响到
“
历史重现
”
,需要考虑兼容之前的事件结构。
-
处理幂等事件:如果对应的事务在执行过程中被中断,需要通过事件回放的方式达到事务的最终一致性问题。此时需要对事件的幂等性提出要求,也就是同一个事件运行多次得到的结果不变。需要在事件处理时丢弃重复事件。
-
查询事件数据库(
event store
)
:由于数据库中存放的一个个事件,如果针对实体状态的查询会相对困难。需要将这些事件重放,获取最新的实体状态的信息。这也是为什么需要通过CQRS的方式将读写进行分离,
Command
端使用
Event
Sourcing
而Query端使用
Event
Sourcing
发出Event
的最终状态进行查询的原因。
CQRS与Event Sourcing的 完美结合
通过上面对Event
Sourcing
的介绍,可以发现它针对Event
进行记录存放到Event
Store中,并且把最终的状态放到视图中进行保存可以供给
Query
端进行查询。这种模式天生与
CQRS
就有默契的配合。
从CQRS模式的结构看,实体状态的变化发生在
Command
端,
Command
端知道业务处理进行了哪些具体操作,将这些具体的操作进行封装就形成了
Event
。
而
Query
端,查询返回的是实体当前状态状态。根据“当前状态
+
变化
=
新的状态”,如果能从
Command
端得到“变化”,再加上
Query
端自身获取的“当前状态”就能得到变化后的“新的状态”。
此时
Command
端发出的
Event
正好符合这个“变化”,如果当变化发生也就是新
Event
产生时,由
Command
端将这个
Event
推送到
Query
端,
Query
端根据
Event
刷新状态,就能保证两端实体状态一致,达到最终一致性,
如图4所示:
图
4
Event
Sourcing
和 CQRS
结合
在图3的基础上加入
Event
Handler
也就是图中蓝色部分,这部分接收从Domain
Model中发过来的
Event
信息,也就是最新的实体修改信息。再将这个信息存放到
Reader
Database(也可以理解为视图)中,这样新的
E
v
ent
信息加上当前的实体信息就时最新的实体信息了。而采用这种方式以后Query
端依旧可以通过Reader
Database获取数据对其原来的操作并没有产生影响。
再回到Command端,其对应的多次操作的
Event
会存放到Event
Store中,作为业务跟踪的记录被保存下来。
上面提到的只是一种系统架构的模式,在实际运用中可以根据具体情况进行改进和优化。如图
5
所示,可以在
Command
端和
Query
端进行
Event
交换的时候加入队列,满足两套应用程序部署在不同进程的场景需求。
图5
Command
端和Query
端加入队列
上面聊到了CQRS与
Event
Sourcing的完美结合,这里通过一个例子给大家进一步介绍其运作的过程。这个例子的背景是,对于用户(
User
)
而言保存了对应的联系方式(Contact)和住址(
Address
)。
Command
用来建立(Create)用户(
User
)
和更新(Update)用户(
User
);
Query
用来查询用户(User)对应的住址(
Address
)和联系方式(
Contact
)。
如图
4
所示,
Client
请求应用分为上线两条线,分别用四种颜色代表。我们根据不同颜色来讲解
Command
端和
Query
端执行的过程。
图
4
Event
Sourcing
和 CQRS
结合
红色向左的线:这里主要是针对User
的create
和update
操作,分别填充CreateUserCommand类和
UpdateUserCommand
类,作为
UserAggregate
聚合类的输入参数。在
UserAggregate
中分别由,
handleCreateUserCommand
和
handleUpdateUserCommand
两个方法处理,最后通过
UserWriteRepository
来保存到
Write
database中。
-
绿色向下的线:其连接了紫色的区域是UserProjection,它的作用是将
Write
database的数据同步到
Read
database中。
-
蓝色向右的线:Client
发起Query请求通过
AddressByRegionQuery
类和
ContactByTypeQuery
类构建请求,将其传送到
UserProjection
类进行处理,其中
handle
方法分别对两类参数的请求进行处理。最后通过
UserReadRepository
获取
Read
database中的信息。
-
紫色向左的线:当从Read
database
中获取信息以后,返回给Client。
如图
7
所示,
User
实体类包括如下几个字段,也就是我们要操作的业务实体。包括用户的基本信息,其中
contact
和
address
类的具体信息在这里不展开描述。
Command
的类信息如图
8
所示,其内容相对简单。针对
CreateUserCommand
主要用于创建用户,包括
UserID
和
FirstName
以及
LastName
。
如图9所示,
UpdateUserCommand
中加入了地址和联系方式的更新内容。