CQRS架构模式

概述

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
// 简单DTO查询
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
// Command发送
[HttpPost]
public ActionResult Add(DiaryItemDto item)
{
ServiceLocator.CommandBus.Send(
new CreateItemCommand(Guid.NewGuid(), item.Title, ...)
);
return RedirectToAction("Index");
}

// Command处理
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可获得完整的历史追溯能力。关键成功因素包括:严格的单聚合根修改、可靠的幂等处理、以及高效的事件分发机制。