当前位置: 首页 > 科技观察

一篇文章带你了解CQRS模式

时间:2023-03-12 21:19:22 科技观察

背景问题简单需求当我们系统中数据模型层级较小,数据模型足够简单时,可以直接将模型和数据库进行映射。这种简单的数据模型无需对其相互关系进行复杂的建模和设计,直接在工程中使用经典的三层模型就足以支持项目需求。对于这样一个简单的系统,过度设计会增加后续维护和改造的成本(不保证前期设计能完美满足后续需求)。同时,对于简单的系统,我们的大部分需求只涉及少量的数据模型逻辑处理。但是我们可以直接对数据模型进行CURD来满足需求,结论是:对于简单的需求,不需要区分查询和增删改查的程序结构。复杂的需求如果我们的系统有一定的复杂性,这个复杂性可能是因为访问的频率,数据量,或者数据模型的数量。这时候我们遇到的问题是,数据查询和更新需求之间的差距逐渐变大了。频率:数据的查询频率会远高于新增、更新、删除的频率。数据量:当数据量变大时,对分库分表的设计要求会增加,导致数据查询复杂化(涉及分表关键字)。数据模型数量:数据模型数量的增加会增加增、更新、删除操作时同时受影响的数据模型数量,同时跨多个模型的查询条件会大大提高性能查询具有挑战性。根据上面的例子,我们可以发现,当我们的需求具有一定的复杂性时,根据引入的复杂性,会导致更复杂的设计来支持需求在系统功能方面的复杂性。同时我们也可以发现,增删改查引入的不同复杂度所带来的功能需求也大不相同。因此:需求的复杂性会放大程序中查询和增删改查的设计差异。DDD要求如果我们对系统的整体构建和设计有更高的可维护性和可扩展性要求,那么我们就需要使用DDD来设计整个系统。在这种情况下,模型往往具有相对复杂的模型关系。在增删改查的时候,我们需要将所有的请求都封装成领域对象,这样程序才能基于领域模型完成大量复杂的验证和业务逻辑。在查询需求时,我们往往需要对跨域的数据进行整理,以列表的形式完成数据内容的展示。因此:在DDD设计中,增删改查操作很容易在应用领域模型中执行,而查询操作往往不能直接通过领域模型执行。CQRS模式问题的抽象根据第一节的内容,我们可以发现,在设计系统架构时,当系统变复杂时,有一个核心问题:增删改查功能的类型和功能查询类型有功能要求。巨大差距。这种差异的直接结果就是在系统开发过程中,对于增删改查操作的业务设计会有比较大的差异。如果举几个例子,比如:对于增删改查系统,我们需要事务来保证多领域模型的更新原子性;对于查询,我们需要增加缓存来提高热点数据的查询性能。数据读写的模型通常不匹配,他们维护和查询的列或属性坑之间没有交集。在更新时查询数据可能会产生冲突。使用统一模型进行存储可能会导致复杂查询的性能降低。CQRS的本质是增删改查逻辑是有区别的。为了更好的抽象出差异,我们可以分开设计。那就是我们的CQRS模式,也就是命令查询责任分离CommandQueryResponsibilitySegregation模式。其中,我们把增删改称为命令式操作。CQRS本质上是一种读写分离的设计思想。该框架设计模式将命令型业务和查询型业务分开处理。通过这种方式,CQRS可以独立设计命令和查询的业务模型,从而提供更适合各自场景的解决方案和组件的能力。查询查询操作不会修改数据库中的内容,所以查询本身就是一个幂等操作。在不改变系统的情况下重复执行相同的查询条件,将返回相同的结果。我们可以为这个特性提供数据Cache,以提高系统性能;同时,由于不影响数据库,查询逻辑不会造成数据一致性问题。查询往往具有较高的使用频率。命令操作会直接修改数据库,我们需要为多个领域模型增加操作的原子性。对于命令操作,我们往往不直接依赖于命令的返回值,所以命令操作通常可以异步执行。对于一般的系统,命令操作往往用得较少。简单实用由于CQRS的本质是读写操作分离,所以比较简单的CQRS做法是:CQ两端的数据库表是共享的,CQ两端只在上层代码上分开。这样,在不分离数据库设计的情况下,在上层代码中将CQ的两端分开,分别维护。比如command类型由xxxManagerController和xxxManagerService定义,而query直接由xxxController和xxxService定义。因为使用同一个数据库,所以CQ两端不存在数据一致性问题。但是因为已经抽取了上层代码,可以满足一些设计特点,比如:命令应该以任务为中心,而不是以数据为中心。可以将命令放在队列中进行异步处理,而不是同步处理。查询从不修改数据库。查询返回的DTO不封装任何领域知识。该方案可以满足代码的逻辑分离和维护,但由于使用同一张数据库表,无法根据两个CQ业务的特点独立设计模型。注重性能在代码分离的基础上,我们可以在物理上分离数据存储模型。读取存储可以是写入存储的只读副本。使用多个只读副本可以提高查询性能;也可以单独读取模型设计库表。独立建模查询和更新可以降低设计和实现的难度。而此时读数据库可以使用自己为查询优化过的数据结构。例如读数据库可以直接将查询数据存储在宽表中,避免join操作或复杂的查询映射。您甚至可以使用mongo或es等nosql数据库来增强读取操作的查询逻辑。分离出来的数据会存在于不同的数据库中,Q数据会从C端同步过来。通常,这是通过让写入模型在每次更新数据库时发布一个事件来实现的。说到数据同步,有两种方案:同步执行和异步执行:同步:导致性能下降,但可以保证数据的强一致性。异步:具有高性能,但要求系统接受最终一致性。同样,这种同步也可以理解为对缓存的更新,即查询数据库使用缓存,而写入数据库使用普通MySQL,两者之间的数据同步通过领域事件实现最终一致性。进一步加强和进一步,由于命令操作实际上是对“操作”的记录,只需要查询就可以汇总显示所有的操作。基于这个思路,可以使用EventSourcing方式来记录命令操作。在该方案中,保存记录时更新的不是当前记录,而是会引起状态变化的事件日志。每个事件代表对数据所做的一系列更改,我们可以通过重放事件来构造数据的当前状态(可以参考Mysql的Binlog设计)。这种录音的好处是可以根据回放再现每次状态变化的时间点和变化轨迹。另一方面,查询可以根据当前状态的快照加速查询。架构图来自网络:这种设计模式听起来很复杂,但是它有很多好处,例如:实现透明的分布式处理,当使用事件作为状态变化的引擎时,可以实现多任务并发处理,比如通过JVM并行计算或事件消息总线机制,事件可以很容易地序列化并在多个服务器之间传输。同时,由于是保留的操作记录,回放时可以过滤异常操作数据,增加数据的健壮性。使用挑战如果要使用CQRS,根据想要达到的系统性能,需要评估当前的系统架构和个人经验是否具备以下能力:复杂性设计:虽然CQRS的基本概念比较容易理解,这种模式会导致系统的构建复杂度上升,尤其是在进一步使用EventSourcing模式时。消息队列处理:在设计高性能时,通常会用到消息处理命令和更新事件。在这种情况下,应用程序必须处理消息失败或重复消息。最终一致性:如果将读取和写入数据库分开,读取的数据可能会过时。必须更新读取模型存储以反映对写入模型存储所做的更改,并且很难检测用户何时基于过时的读取数据发出请求。选型建议以下场景不建议引入CQRS:领域或业务非常简单。基本的CRUD可以支持完整的系统数据访问需求。如果系统具有一定的复杂性,具有以下特点,可以根据特点选择合适的CQRS实现方式。在用户操作中,需要在用户界面中进行一系列复杂的操作,最终定义、组装、修改领域模型。编写模型需要完整的命令处理栈,包括:输入验证、业务处理、业务验证。读模型只需要返回视图中使用的DTO数据即可。读写模型只需要最终一致性关系。对于用户的操作访问,需要定义更细粒度的命令,并通过合并命令来避免命令冲突。数据写入和数据读取存在比较大的性能差异,需要单独进行数据优化。特别是在读次数远大于写次数的场景下,读模型可以进行横向扩展。当团队成员可以分为专门负责复杂业务编写场景的组和专门负责高频查询和用户界面的组时。当系统随时间演变,包含模型的多个版本,或者业务规则被定期修改时。模型的多个版本可以包含在写入模式中,而统一的视图模型用于读取模式。与其他系统集成时,希望不受其他系统故障影响(读写库表分离)。最后,总的来说,CQRS是一种处理复杂问题的具体实现方案,常与DDD结合使用。总结CQRS的主要优势包括:独立扩展:CQRS允许读取和写入工作负载独立扩展,这可能会减少锁争用。优化的数据模式:读取端可以使用针对查询优化的模式,写入端可以使用针对更新优化的模式。安全性:更容易确保只有正确的域实体写入数据。关注点分离:分离读写端使模型更易于维护和灵活。大多数复杂的业务逻辑被划分到写入模型中。阅读模型变得相对简单。更简单的查询:通过将物化视图存储在读取数据库中,应用程序可以避免在查询时进行复杂的连接。