在日常工作中,我们经常会遇到各种挑战,尤其是在系统设计和性能优化方面。以下是一些常见的问题及其背后的原因分析,以及CQRS架构的介绍和其在降低系统复杂性中的作用。

常见问题与挑战

  1. 接口性能问题 使用已有接口进行业务开发时,可能会遇到性能瓶颈,尤其是在高并发场景下。例如,如果接口全部依赖数据库操作而没有使用缓存,就可能导致性能问题。
  2. 缓存与数据库一致性问题 开发后台管理功能时,如果缓存与数据库数据不一致,可能会导致业务反馈数据错误,需要考虑缓存策略和数据同步机制。
  3. 功能更新导致的连锁反应 在产品更新过程中,新增功能可能会影响现有流程,导致异常情况发生,需要进行回滚处理。
  4. 数据库瓶颈问题 在高并发场景下,数据库可能成为系统瓶颈。查询性能问题可以通过增加索引解决,但更新性能又可能受到影响。
  5. 系统性能随数据量增长而下降 随着数据量的增加,系统性能可能会下降,尤其是在后台管理中复杂的查询场景下,数据库的Join操作可能导致性能问题。

CQRS架构简介

CQRS,即命令查询责任分离,是一种将写操作(Command)和读操作(Query)分离的架构模式。这种模式不仅是一种业务架构,也是一种设计思维,其核心在于“拆分”。

CQRS的目的

CQRS的目的是降低系统的复杂性,通过分离命令和询操作,使得系统更容易扩展和维护。

CQRS背后的逻辑

假设系统中命令操作的复杂性为M,查询操作的复杂性为N。如果使用同一套模型处理命令和查询,系统的复杂性可能达到M*N。通过CQRS,我们可以针对命令和查询使用不同的模型和策略,从而降低整体复杂性。

结构化内容

  • 问题识别:识别系统中的性能瓶颈和数据一致性问题。- CQRS架构:介绍CQRS的概念、目的和背后的逻辑。- 应用场景:分析CQRS在实际开发中的应用,如何通过分离读写操作来优化系统性能和维护性。 通过上述分析,我们可以看到CQRS架构在解决系统复杂性问题中的重要性。它提供了一种有效的策略,帮助开发者构建更加健壮和可扩展的系统。 图片 在软件设计中,实现模块间的低耦合是提高系统可维护性的关键。然而,当前的设计模式往往存在模块间相互依赖的复杂性,这导致在进行系统升级或维护时,需要投入大量的精力去排查不同模块间的相互影响。例如,一个模块的更改可能会影响到其他模块的功能,使得整个系统的稳定性和可靠性受到影响。 为了解决这一问题,我们可以采用一种新的设计理念,即将命令(Command)和查询(Query)彻底分离。通过这种方式,我们可以将系统的复杂性简化为 M + N 的形式,其中 M 和 N 分别代表命令和查询的独立变更。这意味着,命令的变更将不会对查询产生影响,反之亦然。这种分离策略不仅降低了系统的耦合度,还提高了代码的可读性和可维护性。 具体来说,我们可以将系统分为两个主要部分:
  1. 命令处理:负责处理用户输入的命令,执行相应的操作,如数据的创建、修改和删除等。
  2. 查询处理:负责响应用户的查询请求,返回所需的数据或信息,不涉及数据的修改。 通过这种分离,系统的各个部分可以独立地进行开发和优化,而不必担心对其他部分产生不利影响。这将大大提高开发效率,同时也使得系统的扩展和维护变得更加容易。 图片

image

当然,以上两个极端在实际工作中也很少见,通常系统的复杂性介于两者之间。

图片

image

这只是从理论进行推导,在实际工作中随处可见的“冲突”也是对“拆分”的一种暗示。

2. 分层架构中的冲突

以最常见的分层架构进行介绍,具体如下:

图片 在现代软件架构设计中,分层是一种常见的方法,它有助于将系统的不同部分清晰地分离开来,从而提高系统的可维护性和扩展性。以下是对系统分层的详细描述,按照结构化和条理化的方式重新编写:

系统分层架构

1. Web接入层Web接入层是系统的前端,主要负责处理用户输入。它对输入信息进行验证,调用应用服务来完成业务操作。验证通过后,将结果进行转换,并最终返回给用户。

2. 应用服务层应用服务层是业务流程的核心,负责业务逻辑的编排。它从仓库层获取领域对象,执行业务操作,并通过仓库同步最新的对象状态到数据存储引擎。此外,这一层还负责发布领域事件,以响应业务变化。

3. 领域层领域层是业务逻辑的集中体现,通常基于面向对象设计。它利用封装、继承和多态等特性,确保业务逻辑的复用性和扩展性。

4. 仓库层仓库层是数据访问的中介,为应用服务层提供数据操作服务,同时屏蔽不同存储引擎之间的差异。

5. 数据层数据层负责数据的保存和检索。常见的数据存储引擎,如MySQL、Redis、Elasticsearch(ES)等,都属于这一层。

CQRS架构下的纵向拆分

除了横向的分层,还可以通过CQRS(命令查询责任分离)原则对系统进行纵向拆分。这种拆分将每个层的组件分为Command和Query两部分:

  • Command 负责处理写操作,如创建、更新和删除数据。- Query 负责处理读操作,如查询数据。

应用服务层的拆分在应用服务层,将服务拆分为CommandService和QueryService,以实现更清晰的职责分离:

  • CommandService 处理写操作,执行业务逻辑,并触发领域事件。- QueryService 处理读操作,提供数据查询服务。

接入层的拆分建议虽然接入层的冲突较小,拆分的意义不大,但从严格意义上讲,建议进行拆分以保持架构的一致性和清晰度。

结论分层架构通过将不同的关注点封装在不同的层次,提高了系统的可维护性和扩展性。CQRS原则进一步强化了这种分离,使得系统更加灵活和高效。

图片

image

这样做可以避免很多不必要的麻烦,Command 和 Query 存在较大的区别,具体如下:

|

CommandService QueryService
依赖组件不同 ValidateService 验证服务;LazyLoaderFactory 延迟加载服务;CommandRepository 不带缓存的仓库;EventPublisher 事件发表器 QueryRepository 带缓存功能的仓库;JoinService 数据聚合服务;Converter 数据转换服务
核心流程不同 验证、加载、业务操作、同步、发布事件 验证、加载、数据组装、转换
功能加强不同 主要是事务管理器 主要是缓存组件

回想开篇时提到的场景,完成应用层拆分,就不在为使用错组件而烦恼:

  1. CommandService 的 Repository 不使用缓存,仅操作数据库

  2. QueryService 的 Repository 可以使用缓存,以提升访问性能

除此之外,针对统一的操作流程,还可以进一步抽象来消除重复的“模板代码”,比如:

  1. 引入“模板方法设计模式” 以达到核心逻辑的复用

  2. 抽象出 BaseCommandService 和 BaseQueryService 两个父类用于统一核心流程

  3. 子类实现 BaseCommandService 和 BaseQueryService 的抽象方法完成功能扩展

  4. 基于“约定优于配置” 使用 Proxy 模型,只定义接口不写实现代码

  5. 按规范定义 CommandService 和 QueryService 接口,通过注解完成相关配置

  6. 自动生成 Proxy 实现类,完成流程编排

4. 模型层冲突与拆分

模型层是系统的核心,它的设计直接影响整个系统的质量。作为承接业务逻辑的核心,比较流程的实现策略包括:

  1. DDD 领域驱动设计,其核心是使用面向对象的高级特性(封装、继承、多态、组合等)来进行设计,非常适合复杂的业务场景。其体现就是存在很多高内聚低耦合的对象组(聚合根),业务逻辑由这些小对象相互协作共同完成;

  2. 事务脚本,使用过程式思维,将数据操作编织到流程中,比较适合并不复杂的业务场景。其体现就是存在很多“上帝 Service”,Service 中存在很多非常长的方法,业务逻辑由这些方法完成;

关于哪个才是最优解,网上已经争论多年,最终也没有结论。但我始终认为“没有业务场景就讨论方案,就是在耍流氓”。

从不同应用场景出发便可得到如下结论:

  1. Command 场景需要保障严谨的业务逻辑,通常复杂性偏高,所以DDD 是最优解

  2. Query 场景需要更灵活的数据组装能力作为支持,通常比较简单,所以 事务脚本 是最优解

我经常说:“最简单的“写”也是复杂,最复杂的“读”也是简单”,其背后逻辑是基于对 Command 和 Query 的场景判断。

将模型拆分为 Command 和 Query,具体如下:

图片 在软件开发中,模型拆分是一种提高代码可维护性和可扩展性的有效手段。以下是对模型拆分特征的详细解析:

聚合根(Agg)与领域驱动设计(DDD)聚合根是DDD中的核心概念,它作为领域模型的入口点,负责协调内部实体和值对象,处理复杂的业务逻辑。聚合根通常表现为具有丰富业务操作的“富对象”。

视图对象(View)视图对象是标准的POJO,作为查询结果的载体,通常不包含业务逻辑,仅用于数据展示。它们是典型的“贫血对象”,根据需求对数据进行组装。

视图与聚合根的转换视图对象不直接访问数据,而是依赖于聚合根通过CommandRepository获取数据。Converter组件负责将聚合根模型转换为视图模型。

实例分析以电商订单模块为例,我们可以将模型拆分为以下几类:

  • 订单业务操作(OrderBO):采用DDD建模,对外提供统一的业务操作接口,对内协调OrderItem和PayInfo等实体。- 订单列表视图(OrderListVO):作为POJO,包含Order和User信息,用于展示订单列表。- 订单详情视图(OrderDetailVO):同样作为POJO,属性更丰富,包含Order、User、Address等信息,用于展示订单详情。

模型独立性通过这种拆分,每个模型独立于其他模型,互不影响,便于维护和扩展。

仓库层的拆分仓库层的拆分有助于解决底层实现、方法复杂性和返回值的差异。以下是仓库层拆分的对比:

  • CommandRepository:主要基于数据库实现,提供少量但关键的方法,如save、update、getById等。- QueryRepository:可能基于数据库、Redis、Elasticsearch等多种存储引擎实现,方法多样,返回值根据业务场景定制。

架构概览整体架构在拆分后更为清晰,每个组件负责特定的职责,提高了系统的可维护性和可扩展性。

注意:以上内容仅为模型拆分的概述,具体实现可能根据项目需求有所不同。 图片

仓库拆分的特点

仓库拆分是一种优化软件架构的策略,它具有以下显著特点:

  1. 视图独立性:视图(View)不再依赖于转换器(Converter)进行数据转换,从而提高了数据展示的灵活性和效率。2. 数据定制化:视图的数据直接来源于其对应的仓库(Repository),可以根据不同展示需求进行定制化处理。3. 命令与查询一致性:命令(Command)和查询(Query)依然使用相同的数据库和数据表,保证了数据操作的一致性。

数据层拆分的重要性

数据层拆分是整个系统架构中最为关键的一环,通常也是我们首先考虑的拆分点。其核心原因在于不同数据存储引擎在最佳应用场景上的差异极大,尤其是在读和写优化方面。

数据层拆分的策略

  1. 查询性能优化:为了提升查询性能,建议为各种查询维度建立索引,以加快数据检索速度。2. 写入性能优化:提升写入性能通常需要减少表上的索引数量,以降低写入时的负担。3. 更新性能优化:为了加速更新操作,建议使用三范式设计表结构,减少数据冗余,简化数据关系。4. 查询性能进一步优化:为了进一步提升查询性能,建议采用反范式设计,通过数据冗余减少数据表间的连接(Join)操作,从而提高查询效率。 在数据库层面,我们经常面临“鱼和熊掌不可兼得”的困境,需要在读写性能、更新性能和查询性能之间做出权衡。

数据层拆分后的架构

数据层拆分后的架构设计需要考虑多种因素,以实现最佳的系统性能和可扩展性。以下是一些关键点:

  • 选择合适的数据存储引擎,以满足不同的读写需求。- 设计合理的索引策略,以平衡查询和写入的性能。- 采用适当的数据模型设计,以减少数据冗余并优化查询效率。 通过这些策略,我们可以构建一个既高效又灵活的系统架构。 图片 在现代大型系统架构中,数据存储的合理拆分是至关重要的。以下是对上述内容的重新整理和结构化:

数据存储拆分的特点

1. 存储引擎的灵活性数据存储彻底拆分,使得Command和Query可以灵活选择最合适的存储引擎。

2. 数据同步机制Command与Query之间需要数据同步,常见的同步机制包括:

  • 应用层基于领域事件的数据同步。- 数据层基于日志的数据同步,例如MySQL的主从同步、Canal等。

3. 订单系统的数据层拆分以订单系统为例,数据层拆分的策略如下:

  • Command侧首选具有ACID特性的关系型数据库,确保一致性。- Query侧引入Redis作为分布式缓存,加速数据访问。- Query侧引入ES进行全文检索,满足复杂查询需求。- Query侧引入TiDB,支持海量数据的实时检索。

4. 数据密集型系统的现状数据密集型系统面临多样化的数据处理和存储需求,需要将工作拆分成多个任务,并通过API对外提供服务。

5. CQRS架构CQRS(命令查询责任分离)架构将业务系统分为Command和Query两部分,针对不同部分寻找最优解决方案。

Command部分- 基于领域驱动设计(DDD)理论,实现战术模型的落地。 - 聚合设计 - 仓库设计 - LazyLoad + Context模式 - 业务验证 - 领域事件

Query部分- 以数据检索和组装为核心能力,设计留给开发人员,实现留给框架。 - QueryObject查询对象模式 - 内存Join模式 - 宽表&冗余表模式

6. 小结拆分是分离关注点的重要手段,通过将问题归类并采取针对性手段,可以更有效地解决问题。

7. 结构化内容以上内容已按照逻辑结构重新编排,以Markdown格式呈现,便于理解和应用。