概述
CQRS(Command Query Responsibility Segregation)是一种将命令(写操作)和查询(读操作)职责分离的架构模式。通过分离读写模型,实现系统在性能、可扩展性和安全性方面的针对性优化。
核心思想:任何方法可分为两类:
- 命令(Command):改变系统状态,无返回值
- 查询(Query):返回数据,不改变系统状态
一、传统CRUD的问题
1.1 主要痛点
| 问题 |
说明 |
| 粗粒度实体 |
读写使用同一实体,导致不必要的字段传输 |
| 资源竞争 |
读写混合引发锁竞争,影响吞吐量 |
| 性能瓶颈 |
直接数据库交互在高并发下成为瓶颈 |
| 权限复杂 |
同一实体需处理读写不同权限 |
1.2 读写频率失衡
- 多数系统 读 >> 写(如100:1)
- 传统架构无法针对读/写路径独立优化
二、CQRS核心架构
2.1 基础结构
1 2 3 4 5 6 7
| [Command Side] [Query Side] │ │ ▼ ▼ [Write Database] ←同步/异步→ [Read Database] │ │ ▼ ▼ [Domain Events] → [Event Handlers] → [Reporting DB]
|
2.2 关键组件
| 组件 |
职责 |
| Command Bus |
路由命令到对应处理器 |
| Command Handler |
执行业务逻辑,产生领域事件 |
| Event Store |
持久化领域事件(Event Sourcing) |
| Event Bus |
分发事件到处理器 |
| Read Model |
专为查询优化的数据模型 |
三、适用场景
3.1 推荐使用
- 复杂业务逻辑:需要清晰分离读写模型
- 高性能需求:读写负载差异大,需独立扩展
- 任务驱动UI:基于工作流的用户交互
- 团队分工:不同团队负责读/写模块
- 事件溯源集成:需要完整操作历史
3.2 不推荐使用
- 简单CRUD:业务逻辑简单,增加复杂度得不偿失
- 全系统滥用:仅特定模块适合CQRS
四、高性能实现策略
4.1 避免资源竞争
单聚合根修改原则
- 每个Command只修改一个聚合根
- 多聚合根操作通过Saga模式实现最终一致性
命令排队机制
1 2 3 4 5
| graph LR A[Command] --> B{路由到服务器} B --> C[内存队列] C --> D[单线程处理] D --> E[持久化事件]
|
4.2 幂等性保障
| 环节 |
实现方式 |
| Command |
CommandId主键约束 |
| Event |
(AggregateId + Version)联合主键 |
| Event消费 |
处理记录表 + 先查后处理 |
4.3 存储优化
- 分库分表:按聚合根类型+ID哈希拆分
- Group Commit:批量持久化事件(50-100个/批)
- In-Memory模式:聚合根常驻内存,异步持久化
五、代码实现示例
5.1 查询侧(Q端)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public ActionResult Index() { ViewBag.Model = ServiceLocator.ReportDatabase.GetItems(); return View(); }
public class ReportDatabase : IReportDatabase { static List<DiaryItemDto> items = new List<DiaryItemDto>(); public List<DiaryItemDto> GetItems() => items; public void Add(DiaryItemDto item) => items.Add(item); }
|
5.2 命令侧(C端)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| [HttpPost] public ActionResult Add(DiaryItemDto item) { ServiceLocator.CommandBus.Send( new CreateItemCommand(Guid.NewGuid(), item.Title, ...) ); return RedirectToAction("Index"); }
public class CreateItemCommandHandler : ICommandHandler<CreateItemCommand> { public void Execute(CreateItemCommand command) { var aggregate = new DiaryItem(command.Id, ...); _repository.Save(aggregate, aggregate.Version); } }
public class DiaryItem : AggregateRoot { public DiaryItem(Guid id, string title, ...) { ApplyChange(new ItemCreatedEvent(id, title, ...)); } public void Handle(ItemCreatedEvent e) { Title = e.Title; Id = e.AggregateId; } }
|
5.3 事件持久化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class InMemoryEventStorage : IEventStorage { public void Save(AggregateRoot aggregate) { foreach (var @event in aggregate.GetUncommittedChanges()) { @event.Version = ++version; _events.Add(@event); } foreach (var @event in aggregate.GetUncommittedChanges()) { _eventBus.Publish(@event); } } }
|
六、CQRS与Event Sourcing
6.1 协同关系
- CQRS:分离读写模型
- Event Sourcing:以事件序列作为唯一数据源
- 组合优势:
6.2 数据流
1 2 3
| Command → Command Handler → Domain Events → → Event Store(持久化) → Event Handlers → → Update Read Models
|
七、实施建议
7.1 架构选择
| 方案 |
特点 |
适用场景 |
| 共享数据库 |
代码分离,数据一致 |
中小型系统 |
| 独立数据库 |
读写完全分离 |
高并发系统 |
7.2 技术栈
- 消息队列:EQueue/RabbitMQ/Kafka(确保可靠投递)
- 存储:关系型DB(MySQL/PostgreSQL)或NoSQL
- 监控:跟踪Command/Event处理延迟
7.3 运维要点
- 最终一致性:接受Q端数据延迟(通常<1s)
- 补偿机制:Saga失败时的回滚策略
- 版本兼容:事件结构的向后兼容设计
总结
CQRS通过读写分离解决了传统CRUD架构的性能与复杂度问题,特别适合高并发、业务复杂的系统。实施时需权衡复杂度收益,结合Event Sourcing可获得完整的历史追溯能力。关键成功因素包括:严格的单聚合根修改、可靠的幂等处理、以及高效的事件分发机制。