从数据库读写分离到CQRS
1. 数据库读写分离
对于数据库的操作就四种:CRUD
我们把这四种操作,又划分为两类,读和写
当我们的系统并发量高的时候,自然会考虑到提高数据库性能,数据库读写分离,
但是,实际测试下来,总是有各种不满意的地方。其中最麻烦的就是各种复杂查询的性能,写库有单点故障问题
2. CQRS
有了数据库层面的”归纳“:CRUD->读、写, 可以继续”归纳“:CRUD->读、写 --> 查询、命令
将查询和命令分开,也就是我们要说的CQRS(Command Query Responsibility Segregation)模型,命令与查询职责分离
这样,我们在应用层面就可以抽象成如下模型:
乍一看,这套东西其实和采用的数据库的读写分离是一样的,就是把读写给分开,但是这些并没有那么简单
其实是模型的不同,原来的数据库读写分离确实把读写的这两个行为分开了,但是它依然有一个重要的事情没有做,那就是职责的分开
什么叫职责的分开呢?就是读写双方不要搞同一套模型。而数据库读写分离的问题就在这里,它使用了同一个模型。
使用同一个模型在这里造成的问题是,这个模型由于既要考虑读取数据不能太困难,也要考虑写入数据不能太困难。
使用 CQRS 思想的话,写入不需要关心读取的问题,读取数据也不用关心写入的问题,那就可以做到真正的读写分离,提高性能
写存储可以用MySQL这样的关系型数据库,而查询存储则可以使用Elasticsearch作为存储。命令-查询职责分离(CQRS)模式是一种应用于这种场景的通用模型,它显式地将系统中的读(查询)和写(命令)进行分离
优点:
1. 拆分了这两块的代码,使各自可以采用不同的技术栈,做针对性的调优。命令模型负责数据的变更,并把最新数据同步给查询模型。
2. 切分了流量,能够更灵活的做资源分配,处理数据逻辑的时候,查询模型根据自己的想法来安排数据,想怎么用就怎么用
缺点:
1. 引入 CQRS 的模式后,最大的问题在于引入了过度的复杂性, 由于需要读和写分开,那么我们开发的工作量无形中被加大了一倍。又引入 CQRS,这变得更复杂了, 查询想要更好的性能可能就得考虑开源的搜索引擎中间件。每引入一种都会增加开发成本、服务器成本,以及更多的复杂度
2. 最终一致性:如果分离读取和写入数据库,读取数据可能会过时
a. 如果我们采用了CQRS模式,但是命令和查询两侧底层所依赖的数据模型并未分离,而是基于共享的数据存储和数据模型,命令和查询之间不需要额外的交互,命令侧的数据更新对查询侧实时可见。在这种架构模式下,两侧基于共享的数据已经天然的集成在一起,不需要额外机制进行通信,自然也无需引入消息了。
b. 如果我们采用CQRS模式,并且命令和查询两侧进行了数据模型的分离,二者各自依赖独立的数据模型。同时,数据存储也分开部署。命令侧负责数据的更新,而查询侧只负责数据的查询,如何将数据的更新及时同步到查询侧是需要解决的问题。在这种架构模式下,使用消息模式作为两侧的通信机制是个不错的选择
3 种主要的 CQRS 架构
1. 单数据库 CQRS
顾名思义就是command和query都是操作的同一个数据库
2. 双数据库 CQRS
在“双数据库”方式中,我们需要两个数据库,一个用于写操作,一个用于读操作。命令端使用针对写操作优化的数据库。查询端使用针对读取操作优化的数据库
3. 事件源 (EventSourcing) CQRS
最复杂的 CQRS 架构。与前面两种方式相比,事件源存储数据的思路完全不同。在事件源方法中,我们并不只存储实体的当前状态,而且将实体发生的每一个状态作为快照来存储。实体并不是以标准化数据的形式保存,而是通过事件的时间戳来保存它们的变更。
(可以参考Mysql的Binlog设计)这种记录的优点是可以根据回放,重现每一次状态变更的时间点以及变更轨迹。而查询则可以根据当前状态的快照来为查询提速
Event Sourcing也叫事件溯源,是Martin Fowler提出的一种架构模式。其设计思想是系统中的业务都由事件驱动来完成。系统中记录的是一个个事件,由这些事件体现信息的状态。业务数据可以是事件产生的视图,不一定要保存到数据库中
为了便于理解Event Sourcing 我们通过一个例子来进一步解释:
1. 创建了一个银行账户,假设此时的账户ID为“0001”。
2. 针对“0001”这个账户存入300元现金。
3. 然后从“0001”这个账户取出100元现金。
4. 最后,再存入200元。
上面生成的这一系列事件会保存到下方的Event Store的事件库中,这里并不会保存“账户”的状态信息。当需要获取“账户”数据的时候,会通过这些事件信息,还原成“账户”的最终状态,也就是“账户ID”为“0001”,“账户金额”为400。其具体实现方式是,通过账户相关的四个事件对应的处理方法,重新生成当前状态。如果每次查询状态信息都需要这样处理势必会造成资源的浪费,因此在右侧黄色的部分,我们将最终的“账户”信息通过视图的方式保存下来,以供查询
上面这个“账户”处理的过程,就是Event Sourcing,说白了就是通过事件的处理模式。它将系统中的操作都按照事件的方式记录并保存,任何实体的最终状态都是通过事件的叠加和还原确认的
从CQRS模式的结构看
实体状态的变化发生在Command端,Command端知道业务处理进行了哪些具体操作,将这些具体的操作进行封装就形成了Event。
而Query端,查询返回的是实体当前状态状态。根据“当前状态 + 变化 = 新的状态”,如果能从Command端得到“变化”,再加上Query端自身获取的“当前状态”就能得到变化后的“新的状态”
此时Command 端发出的Event正好符合这个“变化”,如果当变化发生也就是新Event产生时,由Command端将这个Event通过EventHandler将这个信息存放到Reader Database(也可以理解为视图)中,这样新的Event 信息加上当前的实体信息就时最新的实体信息了,Query端根据Event刷新状态,就能保证两端实体状态一致,达到最终一致性