名称: CQRS 模式 描述: 实现命令查询职责分离,以在分布式系统中分离读写操作。
CQRS 模式 (命令查询职责分离)
概述
CQRS 将写操作(命令)与读操作(查询)分离,使各自能够独立扩展和演化。它常与事件溯源结合使用,但也可用于传统持久化。
目录
- 什么是CQRS
- 命令与查询
- 分离读写模型
- 无事件溯源的CQRS
- 有事件溯源的CQRS
- 最终一致性
- 读取模型投影
- 同步与异步投影
- 命令处理器
- 查询处理器
- 每个模型的数据库
- 物化视图
- 实现模式
- 何时使用CQRS
- 反模式
什么是CQRS
CQRS 将写模型(命令)与读模型(查询)分离。这允许每侧使用不同的数据形状、存储和扩展策略。
命令与查询
- 命令: 意图改变状态(例如,CreateOrder)。
- 查询: 只读数据请求(例如,GetOrderSummary)。
命令应验证并强制执行不变量;查询应优化快速读取。
分离读写模型
写模型:
- 验证业务规则
- 产生状态变化
- 通常是规范化的
读模型:
- 去规范化以便快速查询
- 可以跨聚合连接数据
CQRS 无事件溯源
使用传统数据库进行写操作和单独的读取副本或视图:
- 写:事务型数据库
- 读:读取优化的副本或缓存
CQRS 有事件溯源
命令侧写入事件。读取模型是从事件构建的投影:
- 清晰的审计日志
- 可重建的视图
- 设计上具有最终一致性
最终一致性
读取可能滞后于写入:
- 向客户端沟通陈旧性
- 需要时使用读取你自己的写入
- 提供一致的状态端点
读取模型投影
投影处理器示例:
interface OrderCreated {
orderId: string;
customerId: string;
total: number;
}
function projectOrderCreated(event: OrderCreated) {
// 插入或更新读取模型
}
同步与异步投影
- 同步: 延迟较低,陈旧度较低。
- 异步: 吞吐量较高,最终一致性。
基于SLA和复杂性选择。
命令处理器
命令处理器应该:
- 验证输入
- 强制执行不变量
- 原子性地持久化更改
- 如果需要,发出事件
查询处理器
查询处理器应该:
- 使用读取优化的存储
- 尽量避免重连接
- 积极分页和缓存
每个模型的数据库
常见模式:
- 写数据库:PostgreSQL/MySQL
- 读数据库:Elastic、Redis 或去规范化表
物化视图
物化视图提供快速读取,并可通过投影或ETL刷新。
实现模式
Node.js 示例结构:
src/
commands/
command-handlers/
queries/
query-handlers/
read-models/
Python 示例:
app/
commands/
handlers/
read_models/
何时使用CQRS
适用场景:
- 读写工作负载差异显著
- 读取模型需要不同形状
- 写侧有复杂业务规则
避免场景:
- 简单CRUD已足够
- 团队无法管理增加的复杂性
反模式
- 为读写共享相同的ORM模型
- 不需要时过度分离模型
- 忽略一致性要求
相关技能
09-microservices/event-sourcing04-database/database-optimization
最佳实践
命令设计
- 使用意图揭示的名称: 命令应清晰表达用户意图
- 验证命令: 处理前验证
- 保持命令小巧: 每个命令单一职责
- 包含命令元数据: 添加时间戳、用户ID等
- 设计为幂等: 命令应安全可重试
查询设计
- 为读取优化: 去规范化数据以快速查询
- 使用适当的存储: 基于查询模式选择存储
- 实现缓存: 缓存频繁访问的数据
- 分页结果: 支持大数据集
- 使用查询模型: 为不同用例优化分离模型
读写分离
- 使用单独的数据库: 读写使用不同存储
- 独立扩展: 分别扩展读写侧
- 处理最终一致性: 接受临时不一致
- 沟通陈旧性: 向用户告知数据新鲜度
- 需要时使用读取你自己的写入: 满足强一致性要求
最终一致性
- 为延迟设计: 假设读取可能滞后于写入
- 提供状态指示器: 向用户显示数据新鲜度
- 使用缓存失效: 更新时使缓存失效
- 实现同步投影: 对需要一致性的关键数据
- 监控延迟: 跟踪写入与读取可用时间
命令处理器
- 验证不变量: 持久化前强制执行业务规则
- 使用事务: 确保原子状态更改
- 发出事件: 状态更改后发布事件
- 优雅处理错误: 返回适当的错误响应
- 记录命令执行: 跟踪命令处理以便调试
查询处理器
- 使用读取优化的存储: 从读取副本或缓存查询
- 避免复杂连接: 去规范化数据以减少连接
- 实现分页: 高效支持大数据集
- 积极缓存: 缓存频繁访问的数据
- 监控查询性能: 跟踪慢查询
数据库配置
- 选择适当的数据库: 匹配数据库到工作负载
- 配置复制: 为查询侧设置读取副本
- 实现连接池: 优化数据库连接
- 使用适当的索引: 为读取模式索引
- 监控数据库健康: 跟踪性能和错误
投影管理
- 使投影可重建: 允许从源完全重建
- 使用增量更新: 事件到达时更新投影
- 处理投影失败: 重试失败投影
- 监控投影延迟: 跟踪投影滞后程度
- 版本投影: 支持多个投影版本
测试
- 测试命令处理器: 验证验证和状态更改
- 测试查询处理器: 验证查询结果和性能
- 测试一致性场景: 测试最终一致性行为
- 测试失败场景: 测试错误处理和恢复
- 性能测试: 测量吞吐量和延迟
监控
- 跟踪命令指标: 监控命令处理率和失败
- 跟踪查询指标: 监控查询性能和模式
- 监控延迟: 跟踪写入与读取可用时间
- 设置警报: 对异常或失败通知
- 创建仪表板: 可视化CQRS系统健康
检查清单
设计
- [ ] 识别读写工作负载差异
- [ ] 设计命令模型
- [ ] 设计查询模型
- [ ] 选择适当的数据库
- [ ] 定义一致性要求
命令侧
- [ ] 实现命令验证
- [ ] 设置命令处理器
- [ ] 配置写数据库
- [ ] 实现事件发布
- [ ] 添加命令日志记录
查询侧
- [ ] 设计读取模型
- [ ] 设置读取数据库/副本
- [ ] 实现查询处理器
- [ ] 配置缓存
- [ ] 添加查询日志记录
最终一致性
- [ ] 定义可接受延迟
- [ ] 实现状态指示器
- [ ] 设置缓存失效
- [ ] 配置同步投影
- [ ] 监控延迟指标
投影
- [ ] 设计投影架构
- [ ] 实现投影处理器
- [ ] 设置增量更新
- [ ] 配置投影重建
- [ ] 监控投影延迟
数据库设置
- [ ] 配置写数据库
- [ ] 配置读取数据库/副本
- [ ] 设置连接池
- [ ] 配置复制
- [ ] 优化索引
缓存
- [ ] 识别可缓存数据
- [ ] 配置缓存策略
- [ ] 设置缓存失效
- [ ] 监控缓存命中率
- [ ] 配置缓存过期
监控
- [ ] 设置命令指标
- [ ] 设置查询指标
- [ ] 配置延迟监控
- [ ] 创建仪表板
- [ ] 设置警报
测试
- [ ] 编写命令处理器测试
- [ ] 编写查询处理器测试
- [ ] 测试一致性场景
- [ ] 性能测试
- [ ] 测试失败场景
文档
- [ ] 文档化命令模型
- [ ] 文档化查询模型
- [ ] 文档化一致性要求
- [ ] 创建运行手册
- [ ] 维护API文档