CQRS架构调研报告
linkerlin/cqrs项目分析
基于Redis List实现双向消息通讯的全栈CMS框架,通过Job层统一处理缓存填充实现更彻底的职责分离

职责分离
命令与查询彻底分离,Service层不包含任何写缓存代码
双向通讯
基于Redis List实现Service与Job层的高效异步通信
统一缓存管理
Job层统一负责缓存写入,避免代码重复和维护复杂性
项目概述
linkerlin/cqrs项目是一个基于CQRS架构的全栈内容管理系统(CMS)框架 [44]。 该项目旨在提供一个实践CQRS设计模式的示例,并展示了如何利用现代技术栈构建一个解耦的、可扩展的应用程序。
核心创新
该项目通过将缓存填充任务完全交由Job层处理,使得Service层无需包含任何写缓存的代码, 从而实现了更彻底的职责分离。同时利用Redis List实现了双向消息通讯机制。
前端技术栈
后端技术栈
CQRS架构基本原理与核心概念
CQRS模式定义
命令查询职责分离(Command Query Responsibility Segregation, CQRS)是一种软件架构模式, 其核心思想在于将应用程序的数据读取操作(查询,Query)和数据修改操作(命令,Command)分离开来, 使用不同的模型进行处理[1] [2]。
CQRS架构模式
这种分离借鉴了Bertrand Meyer提出的"命令查询分离"(Command-Query Separation, CQS)原则, CQS主要应用于对象设计层面,而CQRS将这一思想提升到系统架构层面 [12]。
核心原则:命令与查询职责分离
CQRS架构的核心原则在于严格区分命令(Command)和查询(Query)的职责。 命令是指那些会改变系统状态的操作,例如创建订单、更新用户信息、删除商品等。 查询则是指那些不会改变系统状态,仅用于获取数据的操作。
命令(Command)
- • 改变系统状态
- • 包含业务逻辑校验
- • 不直接返回数据
- • 关注数据一致性
查询(Query)
- • 不改变系统状态
- • 仅用于获取数据
- • 应简单高效
- • 关注查询性能
linkerlin/cqrs项目实现细节
核心架构特点:基于Redis List的双向消息通讯
linkerlin/cqrs项目在CQRS架构的实现上,其核心创新点在于利用Redis List数据结构实现了双向消息通讯机制 [48]。 这一设计选择对于理解项目的整体架构至关重要。
Redis List双向通讯机制
Redis List特性利用
Redis List是一个简单的字符串列表,按照插入顺序排序,支持在列表的两端进行高效的插入和删除操作。
主要操作命令:
- •
LPUSH
:左端插入 - •
RPUSH
:右端插入 - •
LPOP
:左端弹出 - •
RPOP
:右端弹出 - •
BRPOP
:阻塞式右端弹出
实现优势:
- • 轻量级消息队列
- • 高性能和持久化
- • 内置阻塞弹出支持
- • 无需额外中间件
查询(Query)流程解析
在linkerlin/cqrs项目中,查询流程的设计充分体现了CQRS架构的核心思想,即优化读取操作 [49]。
查询流程时序图
缓存命中(Cache Hit)
如果Redis缓存中存在请求的数据,Service层会直接将缓存数据返回给前端。
- • 避免数据库查询
- • 显著提升响应速度
- • 减轻数据库压力
- • 提高系统吞吐量
缓存未命中(Cache Miss)
如果缓存中不存在数据,Service层将查询请求以JSON格式推入Redis List。
- • Job层通过BRPOP消费任务
- • 执行实际数据库查询
- • 将数据写入Redis缓存
- • 异步化处理机制
命令(Command)流程解析
在linkerlin/cqrs项目中,命令流程负责处理那些会改变系统状态的操作,例如创建、更新或删除数据 [51]。
命令处理流程
- 1. Service层接收HTTP请求:识别为命令请求
- 2. 命令序列化:将命令数据转换为JSON格式
- 3. 推入命令队列:使用LPUSH将命令发送到Redis List
- 4. Job层监听:通过BRPOP阻塞获取命令
- 5. 命令处理:执行相应的业务逻辑
- 6. 结果返回:将处理结果推送到返回队列
- 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]。
缓存处理小改款设计
设计优势
- • 严格遵循CQRS:命令查询彻底分离
- • Service层简化:只读缓存,触发任务
- • Job层专业化:专注数据加载和缓存更新
- • 架构更清晰:组件边界明确
实现特点
- • 异步处理:提高系统并发能力
- • 集中管理:缓存策略统一控制
- • 独立演进:Job层可独立优化
- • 易于扩展:支持复杂缓存策略
实现方式
利用现有消息队列机制
linkerlin/cqrs项目中小改款的实现,巧妙地利用了项目中已有的基于Redis List实现的消息队列机制 [57]。 这个机制原本用于在Service层和Job层之间传递命令和处理结果,现在被扩展用于传递缓存填充任务。
消息队列机制特点
消息传递流程:
- 1. Service层LPUSH推送任务
- 2. Job层BRPOP获取任务
- 3. 异步处理不阻塞Service
- 4. FIFO顺序保证
技术优势:
- • 避免引入新中间件
- • 简化系统架构
- • 利用Redis高性能
- • 内置持久化支持
避免重复写入缓存的机制
任务去重与幂等性处理
在将缓存填充任务交由Job异步处理的改款设计中,一个潜在的问题是可能会发生重复的缓存写入操作, 尤其是在高并发场景下或者消息队列出现某些异常时。 为了避免这种情况,引入任务去重和幂等性处理机制至关重要。
任务去重机制
- • 唯一任务ID:由请求ID、数据标识符等组合生成
- • 临时去重存储:Redis键,设置较短过期时间
- • 检查机制:处理前检查ID是否已存在
- • 处理策略:已存在则丢弃或跳过
幂等性处理
- • 多次执行一致性:最终结果一致
- • SET NX命令:仅在键不存在时设置
- • 版本号机制:避免旧数据覆盖新数据
- • 时间戳判断:确保数据最新性
实现要点
由于Service层在缓存未命中时会将请求转入Command流程,并通过消息队列通知Job处理 [60], 如果消息队列本身不保证Exactly-Once语义,那么Job层就需要实现幂等性逻辑来应对可能的重复消息。
通过Job统一管理缓存写入
linkerlin/cqrs项目的小改款设计,其核心思想之一是通过将缓存填充任务完全交由Job层处理, 从而实现缓存写入逻辑的统一管理 [62]。 这种设计从根本上避免了在Service层和Job层中重复编写缓存写入代码的问题。
缓存写入统一管理
统一管理的优势
技术优势:
- • 消除代码重复
- • 集中缓存策略控制
- • 简化维护和升级
- • 统一监控和日志
业务优势:
- • 提高系统可观察性
- • 增强可维护性
- • 便于缓存策略优化
- • 降低错误风险