CQRS架构调研报告
linkerlin/cqrs项目分析

基于Redis List实现双向消息通讯的全栈CMS框架,通过Job层统一处理缓存填充实现更彻底的职责分离

CQRS架构抽象设计图

职责分离

命令与查询彻底分离,Service层不包含任何写缓存代码

双向通讯

基于Redis List实现Service与Job层的高效异步通信

统一缓存管理

Job层统一负责缓存写入,避免代码重复和维护复杂性

项目概述

linkerlin/cqrs项目是一个基于CQRS架构的全栈内容管理系统(CMS)框架 [44]。 该项目旨在提供一个实践CQRS设计模式的示例,并展示了如何利用现代技术栈构建一个解耦的、可扩展的应用程序。

核心创新

该项目通过将缓存填充任务完全交由Job层处理,使得Service层无需包含任何写缓存的代码, 从而实现了更彻底的职责分离。同时利用Redis List实现了双向消息通讯机制。

前端技术栈

React 18 UI渲染与组件化
TypeScript 类型安全与维护性
Vite 快速构建工具
Tailwind CSS 实用优先CSS框架

后端技术栈

NestJS 高效Node.js框架
TypeScript 统一类型系统
Redis 消息队列与缓存

CQRS架构基本原理与核心概念

CQRS模式定义

命令查询职责分离(Command Query Responsibility Segregation, CQRS)是一种软件架构模式, 其核心思想在于将应用程序的数据读取操作(查询,Query)和数据修改操作(命令,Command)分离开来, 使用不同的模型进行处理[1] [2]

CQRS架构模式

graph LR A["Client"] --> B["Command Model"] A --> C["Query Model"] B --> D["Write Database"] C --> E["Read Database"] D --> F["Event Bus"] F --> G["Read Model Update"] style A fill:#e1f5fe,stroke:#0f766e,stroke-width:2px,color:#0f766e style B fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#374151 style C fill:#d1fae5,stroke:#10b981,stroke-width:2px,color:#374151 style D fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#374151 style E fill:#d1fae5,stroke:#10b981,stroke-width:2px,color:#374151 style F fill:#f3e8ff,stroke:#8b5cf6,stroke-width:2px,color:#374151 style G fill:#d1fae5,stroke:#10b981,stroke-width:2px,color:#374151

这种分离借鉴了Bertrand Meyer提出的"命令查询分离"(Command-Query Separation, CQS)原则, CQS主要应用于对象设计层面,而CQRS将这一思想提升到系统架构层面 [12]

核心原则:命令与查询职责分离

CQRS架构的核心原则在于严格区分命令(Command)和查询(Query)的职责。 命令是指那些会改变系统状态的操作,例如创建订单、更新用户信息、删除商品等。 查询则是指那些不会改变系统状态,仅用于获取数据的操作。

命令(Command)

  • • 改变系统状态
  • • 包含业务逻辑校验
  • • 不直接返回数据
  • • 关注数据一致性

查询(Query)

  • • 不改变系统状态
  • • 仅用于获取数据
  • • 应简单高效
  • • 关注查询性能

读写模型分离

在CQRS架构中,读写模型的分离是其核心特征之一。这意味着系统至少包含两个独立的数据模型: 一个用于处理写操作(命令模型),另一个用于处理读操作(查询模型) [15] [16]

读写模型分离的优势

命令模型特点:
  • • 面向领域模型设计
  • • 包含复杂业务逻辑
  • • 确保数据一致性
  • • 采用DDD聚合根概念
查询模型特点:
  • • 面向展示需求优化
  • • 高度非规范化结构
  • • 物化视图定制
  • • 优化查询效率

优势与考量

主要优势

  • 性能提升:读写操作独立优化和扩展[27]
  • 灵活性增强:不同模型可采用不同技术栈[30]
  • 关注点分离:提高开发效率和可维护性[32]
  • 安全性控制:更精细的访问权限管理[33]

实施考量

  • 复杂度增加:需处理独立模型和同步机制[37]
  • 最终一致性:读模型可能存在延迟[39]
  • 数据冗余:增加存储成本和同步开销
  • 适用场景:更适合复杂业务系统[42]

linkerlin/cqrs项目实现细节

核心架构特点:基于Redis List的双向消息通讯

linkerlin/cqrs项目在CQRS架构的实现上,其核心创新点在于利用Redis List数据结构实现了双向消息通讯机制 [48]。 这一设计选择对于理解项目的整体架构至关重要。

Redis List双向通讯机制

sequenceDiagram participant Client participant Service participant RedisList participant Job Client->>Service: HTTP请求(查询/命令) alt 查询请求 Service->>Redis: 检查缓存 Redis-->>Service: 缓存命中/未命中 end alt 缓存未命中或命令请求 Service->>RedisList: LPUSH 任务消息 RedisList->>Job: BRPOP 获取任务 Job->>Database: 执行业务逻辑 Job->>Redis: 更新缓存 Job->>RedisList: 返回处理结果 RedisList->>Service: 获取结果 end Service->>Client: HTTP响应

Redis List特性利用

Redis List是一个简单的字符串列表,按照插入顺序排序,支持在列表的两端进行高效的插入和删除操作。

主要操作命令:
  • LPUSH:左端插入
  • RPUSH:右端插入
  • LPOP:左端弹出
  • RPOP:右端弹出
  • BRPOP:阻塞式右端弹出
实现优势:
  • • 轻量级消息队列
  • • 高性能和持久化
  • • 内置阻塞弹出支持
  • • 无需额外中间件

查询(Query)流程解析

在linkerlin/cqrs项目中,查询流程的设计充分体现了CQRS架构的核心思想,即优化读取操作 [49]

查询流程时序图

graph TD A["HTTP请求"] --> B{"判断请求类型"} B -->|"查询请求"| C["检查Redis缓存"] C --> D{"缓存命中?"} D -->|"是"| E["直接返回缓存数据"] D -->|"否"| F["创建缓存填充任务"] F --> G["LPUSH到Redis List"] G --> H["BRPOP获取任务"] H --> I["执行数据库查询"] I --> J["写入Redis缓存"] J --> K["返回处理结果"] K --> E style A fill:#e1f5fe,stroke:#0f766e,stroke-width:2px,color:#0f766e style B fill:#f3f4f6,stroke:#374151,stroke-width:2px,color:#374151 style C fill:#d1fae5,stroke:#10b981,stroke-width:2px,color:#374151 style D fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#374151 style E fill:#d1fae5,stroke:#10b981,stroke-width:2px,color:#374151 style F fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#374151 style G fill:#fbbf24,stroke:#f59e0b,stroke-width:2px,color:#374151 style H fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#374151 style I fill:#fed7aa,stroke:#f97316,stroke-width:2px,color:#374151 style J fill:#fed7aa,stroke:#f97316,stroke-width:2px,color:#374151 style K fill:#d1fae5,stroke:#10b981,stroke-width:2px,color:#374151

缓存命中(Cache Hit)

如果Redis缓存中存在请求的数据,Service层会直接将缓存数据返回给前端。

  • • 避免数据库查询
  • • 显著提升响应速度
  • • 减轻数据库压力
  • • 提高系统吞吐量

缓存未命中(Cache Miss)

如果缓存中不存在数据,Service层将查询请求以JSON格式推入Redis List。

  • • Job层通过BRPOP消费任务
  • • 执行实际数据库查询
  • • 将数据写入Redis缓存
  • • 异步化处理机制

命令(Command)流程解析

在linkerlin/cqrs项目中,命令流程负责处理那些会改变系统状态的操作,例如创建、更新或删除数据 [51]

命令处理流程

  1. 1. Service层接收HTTP请求:识别为命令请求
  2. 2. 命令序列化:将命令数据转换为JSON格式
  3. 3. 推入命令队列:使用LPUSH将命令发送到Redis List
  4. 4. Job层监听:通过BRPOP阻塞获取命令
  5. 5. 命令处理:执行相应的业务逻辑
  6. 6. 结果返回:将处理结果推送到返回队列
  7. 7. 缓存更新:根据小改款设计更新缓存

Service层与Job层的职责与协作

在linkerlin/cqrs项目中,Service层和Job层各自承担着清晰的职责,并通过Redis List实现高效的协作 [52]

Service层职责

  • 接收HTTP请求:作为系统入口点
  • 处理缓存命中查询:直接返回缓存数据
  • 触发命令处理:将请求转为任务推入队列[53]
  • 响应处理结果:接收Job层返回结果

Job层职责

  • 监听命令队列:通过BRPOP获取任务
  • 执行业务逻辑:处理数据库读写操作
  • 填充更新缓存:负责缓存写入操作[54]
  • 返回处理结果:将结果推送到返回队列

协作模式特点

异步处理

基于消息队列实现松耦合通信

独立扩展

各层可根据负载情况独立扩展

职责分离

Service层轻量响应,Job层专注处理

缓存处理小改款设计与实现

改款背景:Service层与缓存写入职责分离

在传统的CQRS架构实现中,Service层在处理查询请求时,如果遇到缓存未命中(Cache Miss)的情况, 通常的作法是Service层自身负责从数据库加载数据并写入缓存。这种设计虽然直观,但也存在一些固有的问题。

传统设计问题

  • 违反单一职责原则:Service层职责不够纯粹
  • 代码复杂度增加:包含缓存维护和更新逻辑
  • 维护困难:缓存策略变更影响Service层
  • 测试复杂性:需要模拟缓存操作

小改款核心目标

将缓存填充的职责从Service层中剥离出来,使得Service层不再包含任何直接写入缓存的代码 [55]。 这种职责的进一步分离,使得系统架构更加清晰,各个组件的边界更加明确。

设计思路:Cache Miss后数据填充任务交由Job处理

基于将缓存写入职责从Service层剥离的背景,linkerlin/cqrs项目的小改款提出了一个明确的设计思路: 当Service层在处理查询请求时发生缓存未命中(Cache Miss),不再由Service层自身去数据库加载数据并写入缓存, 而是将这个"数据填充缓存"的任务交给独立的Job来处理 [56]

缓存处理小改款设计

graph LR A["Service层"] --> B{"缓存检查"} B -->|"命中"| C["直接返回"] B -->|"未命中"| D["创建缓存填充任务"] D --> E["Redis List"] E --> F["Job层"] F --> G["执行数据库查询"] G --> H["写入Redis缓存"] style A fill:#e1f5fe,stroke:#0f766e,stroke-width:2px,color:#0f766e style B fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#374151 style C fill:#d1fae5,stroke:#10b981,stroke-width:2px,color:#374151 style D fill:#fed7aa,stroke:#f97316,stroke-width:2px,color:#374151 style E fill:#fbbf24,stroke:#f59e0b,stroke-width:2px,color:#374151 style F fill:#fed7aa,stroke:#f97316,stroke-width:2px,color:#374151 style G fill:#fed7aa,stroke:#f97316,stroke-width:2px,color:#374151 style H fill:#d1fae5,stroke:#10b981,stroke-width:2px,color:#374151

设计优势

  • 严格遵循CQRS:命令查询彻底分离
  • Service层简化:只读缓存,触发任务
  • Job层专业化:专注数据加载和缓存更新
  • 架构更清晰:组件边界明确

实现特点

  • 异步处理:提高系统并发能力
  • 集中管理:缓存策略统一控制
  • 独立演进:Job层可独立优化
  • 易于扩展:支持复杂缓存策略

实现方式

利用现有消息队列机制

linkerlin/cqrs项目中小改款的实现,巧妙地利用了项目中已有的基于Redis List实现的消息队列机制 [57]。 这个机制原本用于在Service层和Job层之间传递命令和处理结果,现在被扩展用于传递缓存填充任务。

消息队列机制特点
消息传递流程:
  1. 1. Service层LPUSH推送任务
  2. 2. Job层BRPOP获取任务
  3. 3. 异步处理不阻塞Service
  4. 4. FIFO顺序保证
技术优势:
  • • 避免引入新中间件
  • • 简化系统架构
  • • 利用Redis高性能
  • • 内置持久化支持

Service层:触发缓存填充任务

Service层在缓存处理方面的职责被显著简化,其核心任务是触发缓存填充任务 [58]

  • • 检查缓存是否存在
  • • 构造缓存填充命令
  • • 推送命令到Redis List
  • • 不等待任务完成

Job层:执行数据加载与缓存写入

Job层承担了缓存未命中时数据加载和缓存写入的核心职责 [59]

  • • 监听缓存填充队列
  • • 执行数据库查询
  • • 确定缓存键策略
  • • 写入Redis缓存

避免重复写入缓存的机制

任务去重与幂等性处理

在将缓存填充任务交由Job异步处理的改款设计中,一个潜在的问题是可能会发生重复的缓存写入操作, 尤其是在高并发场景下或者消息队列出现某些异常时。 为了避免这种情况,引入任务去重和幂等性处理机制至关重要

任务去重机制
  • 唯一任务ID:由请求ID、数据标识符等组合生成
  • 临时去重存储:Redis键,设置较短过期时间
  • 检查机制:处理前检查ID是否已存在
  • 处理策略:已存在则丢弃或跳过
幂等性处理
  • 多次执行一致性:最终结果一致
  • SET NX命令:仅在键不存在时设置
  • 版本号机制:避免旧数据覆盖新数据
  • 时间戳判断:确保数据最新性
实现要点

由于Service层在缓存未命中时会将请求转入Command流程,并通过消息队列通知Job处理 [60], 如果消息队列本身不保证Exactly-Once语义,那么Job层就需要实现幂等性逻辑来应对可能的重复消息。

通过Job统一管理缓存写入

linkerlin/cqrs项目的小改款设计,其核心思想之一是通过将缓存填充任务完全交由Job层处理, 从而实现缓存写入逻辑的统一管理 [62]。 这种设计从根本上避免了在Service层和Job层中重复编写缓存写入代码的问题。

缓存写入统一管理
graph TB subgraph "传统设计" A1["Service层"] --> A2["写缓存逻辑"] B1["Job层"] --> B2["写缓存逻辑"] end subgraph "改款设计" C1["Service层"] --> C2["仅读缓存"] C1 --> C3["触发任务"] D1["Job层"] --> D2["统一写缓存"] end style A1 fill:#fecaca,stroke:#dc2626,stroke-width:2px,color:#374151 style A2 fill:#fecaca,stroke:#dc2626,stroke-width:2px,color:#374151 style B1 fill:#fecaca,stroke:#dc2626,stroke-width:2px,color:#374151 style B2 fill:#fecaca,stroke:#dc2626,stroke-width:2px,color:#374151 style C1 fill:#d1fae5,stroke:#10b981,stroke-width:2px,color:#374151 style C2 fill:#d1fae5,stroke:#10b981,stroke-width:2px,color:#374151 style C3 fill:#d1fae5,stroke:#10b981,stroke-width:2px,color:#374151 style D1 fill:#d1fae5,stroke:#10b981,stroke-width:2px,color:#374151 style D2 fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#374151
统一管理的优势
技术优势:
  • • 消除代码重复
  • • 集中缓存策略控制
  • • 简化维护和升级
  • • 统一监控和日志
业务优势:
  • • 提高系统可观察性
  • • 增强可维护性
  • • 便于缓存策略优化
  • • 降低错误风险