命令查询职责分离 - CQRS
概念
CQRS
是一种与领域驱动设计和事件溯源相关的架构模式, 它的全称是Command Query Responsibility Segregation, 又叫命令查询职责分离, Greg Young在2010年创造了这个术语, 它是基于Bertrand Meyer 的 CQS (Command-Query Separation 命令查询分离原则) 设计模式。
CQRS
认为不论业务多复杂在最终实现的时候, 无非是读写操作, 因此建议将应用程序分为两个方面, 即Command(命令)和Query(查询)
命令端:
- 关注各种业务如何处理, 更新状态进行持久化
- 不返回任何结果 (void)
查询端:
- 查询, 并从不修改数据库
CQRS的三种实现
单一数据库的CQRS
命令与读取操作的是同一个数据库, 命令端通过ORM框架将实体保存到数据库中, 查询端通过数据访问层获取数据 (数据访问层通过ORM框架或者存储过程获取数据)
双数据库的CQRS
命令与读取操作的是不同的数据库, 命令端通过ORM框架将实体保存到 写库 (Write Db), 并将本地改动推送到 读库 (Read Db), 查询端通过数据访问层访问 读库 (Read Db), 使用这种模式可以带来以下好处:
- 查询更简单
- 读操作不需要任何的完整性校验, 也不需要外键约束, 可以减少锁争用, 我们可以针对查询端单独优化, 还可以使用刚好包含每个模板需要的数据的数据库视图,使得查询变得更快更简单
- 提升查询端的使用体验
- 由于这种架构将读写彻底分离,由于一般系统是读操作远远大于写操作, 这给我们的系统带来了巨大的性能提升, 极大的提升了客户的使用体验
- 关注点分离
- 读写分离的模型可以使得关注点分离, 使得读模型会变得相对简单
事件溯源 (Event Sourcing) CQRS
通过事件溯源实现的CQRS
中会将应用程序的改变都以事件的方式存储起来, 使用这种模式可以带来以下好处:
- 事件存储中了完整的审计跟踪, 后续出现问题时方便跟踪
- 可以在任何的时间点重建实体的状态, 它将有助于排查问题并修复问题
- 提升查询端的使用体验
- 查询端与命令端可以是完全不同的数据源, 查询端可以针对查询条件做针对应的优化, 或者使用像
ES
、Redis
等用来存储数据, 提升查询效率
- 查询端与命令端可以是完全不同的数据源, 查询端可以针对查询条件做针对应的优化, 或者使用像
- 独立缩放
- 命令端与查询端可以被独立缩放, 减少锁争用
当然事情有利自然也有弊, CQRS
的使用固然会带来很多好处, 但同样它也会给项目带来复杂度的提升, 并且双数据库模式、事件溯源模式 的CQRS
, 使用的是最终一致性, 这些都是我们在选择技术方案时必须要考虑的
设计
上述文章中我们了解到了CQRS其本质上是一种读写分离的设计思想, 它并不是强制性的规定必须要怎样去做, 这点与之前的IEvent
(进程内事件、IIntegrationEvent
(跨进程事件不同, 它并不是强制性的, 根据CQRS
的设计模式我们将事件分成Command
、Query
由于
Query
(查询) 是需要有返回值的, 因此我们在继承IEvent
的同时, 还额外增加了一个Result
属性用以存储结果, 我们希望将查询的结果保存到Result
中, 但它不是强制性的, 我们并没有强制性要求必须要将结果保存到Result
中。由于
Command
(命令) 是没有返回值的, 因此我们并没有额外新增Result
属性, 我们认为命令会更新数据, 那就需要用到工作单元, 因此Command
除了继承IEvent
之外, 还继承了ITransaction
,这方便了我们在Handler
中的可以通过@event.UnitOfWork
来管理工作单元, 而不需要通过构造函数来获取
但MasaFramework
并没有要求必须使用 Event Sourcing 模式
或者 双数据库模式
的CQRS, 具体使用哪种实现, 它取决于业务的决策者
下面就就来看看MasaFramework
提供的CQRS
是如何使用的
入门
- 安装.NET 6.0
- 新建ASP.NET Core 空项目
Assignment.CqrsDemo
,并安装Masa.Contrib.Dispatcher.Events
,Masa.Contrib.Dispatcher.IntegrationEvents
,Masa.Contrib.Dispatcher.IntegrationEvents.Dapr
,Masa.Contrib.ReadWriteSplitting.Cqrs
,Masa.Contrib.Development.DaprStarter.AspNetCore
dotnet new web -o Assignment.CqrsDemo
cd Assignment.CqrsDemo
dotnet add package Masa.Contrib.Dispatcher.Events --version 0.7.0-preview.9 //使用进程内事件总线
dotnet add package Masa.Contrib.Dispatcher.IntegrationEvents --version 0.7.0-preview.9 //使用跨进程事件总线
dotnet add package Masa.Contrib.Dispatcher.IntegrationEvents.Dapr --version 0.7.0-preview.9 //使用Dapr提供pubsub能力
dotnet add package Masa.Contrib.ReadWriteSplitting.Cqrs --version 0.7.0-preview.9 //使用CQRS
dotnet add package Masa.Contrib.Development.DaprStarter.AspNetCore --version 0.7.0-preview.9 //开发环境下协助 Dapr Sidecar, 用于通过Dapr发布集成事件
- 注册跨进程事件总线、进程内事件总线, 修改类
Program.cs
示例中未真实使用DB, 不再使用发件箱模式, 只需要使用集成事件提供的PubSub
能力即可
builder.Services.AddIntegrationEventBus(dispatcherOptions =>
{
dispatcherOptions.UseDapr();//使用 Dapr 提供的PubSub能力
dispatcherOptions.UseEventBus();//使用进程内事件总线
});
- 注册Dapr Starter 协助管理
Dapr Sidecar
(开发环境使用)
if (builder.Environment.IsDevelopment())
builder.Services.AddDaprStarter();
- 新增加添加商品方法, 修改类
Program.cs
app.MapPost("/goods/add", async (AddGoodsCommand command, IEventBus eventBus) =>
{
await eventBus.PublishAsync(command);
});
/// <summary>
/// 添加商品参数, 用于接受商品参数
/// </summary>
public record AddGoodsCommand : Command
{
public string Name { get; set; }
public string Cover { get; set; }
public decimal Price { get; set; }
public int Count { get; set; }
}
- 新增加查询商品的方法, 修改类
Program.cs
app.MapGet("/goods/{id}", async (Guid id, IEventBus eventBus) =>
{
var query = new GoodsItemQuery(id);
await eventBus.PublishAsync(query);
return query.Result;
});
/// <summary>
/// 用于接收查询商品信息参数
/// </summary>
public record GoodsItemQuery : Query<GoodsItemDto>
{
public Guid Id { get; set; } = default!;
public override GoodsItemDto Result { get; set; }
public GoodsItemQuery(Guid id)
{
Id = id;
}
}
/// <summary>
/// 用于返回商品信息
/// </summary>
public class GoodsItemDto
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Cover { get; set; }
public decimal Price { get; set; }
public int Count { get; set; }
public DateTime DateTime { get; set; }
}
- 新增
Command
处理程序, 添加类CommandHandler.cs
public class CommandHandler
{
/// <summary>
/// 将商品添加到Db,并发送跨进程事件
/// </summary>
/// <param name="command"></param>
/// <param name="integrationEventBus"></param>
[EventHandler]
public async Task AddGoods(AddGoodsCommand command, IIntegrationEventBus integrationEventBus)
{
//todo: 模拟添加商品到db并发送添加商品集成事件
var goodsId = Guid.NewGuid(); //模拟添加到db后并获取商品id
await integrationEventBus.PublishAsync(new AddGoodsIntegrationEvent(goodsId, command.Name, command.Cover, command.Price,
command.Count));
}
}
/// <summary>
/// 跨进程事件, 发送添加商品事件
/// </summary>
/// <param name="Id"></param>
/// <param name="Name"></param>
/// <param name="Cover"></param>
/// <param name="Price"></param>
/// <param name="Count"></param>
public record AddGoodsIntegrationEvent(Guid Id, string Name, string Cover, decimal Price, int Count) : IntegrationEvent
{
public Guid Id { get; set; } = Id;
public string Name { get; set; } = Name;
public string Cover { get; set; } = Cover;
public decimal Price { get; set; } = Price;
public int Count { get; set; } = Count;
public override string Topic { get; set; } = nameof(AddGoodsIntegrationEvent);
}
- 新增
Query
处理程序, 添加类QueryHandler.cs
public class QueryHandler
{
/// <summary>
/// 从缓存查询商品信息
/// </summary>
/// <param name="query"></param>
/// <returns></returns>
[EventHandler]
public Task GetGoods(GoodsItemQuery query)
{
//todo: 模拟从cache获取商品
var goods = new GoodsItemDto();
query.Result = goods;
return Task.CompletedTask;
}
}
- 新增添加商品的跨进程事件的处理服务, 修改
Program.cs
app.MapPost(
"/integration/goods/add",
[Topic("pubsub", nameof(AddGoodsIntegrationEvent))]
(AddGoodsIntegrationEvent @event, ILogger<Program> logger) =>
{
//todo: 模拟添加商品到缓存
logger.LogInformation("添加商品到缓存, {Event}", @event);
});
// 使用 dapr 来订阅跨进程事件
app.UseRouting();
app.UseCloudEvents();
app.UseEndpoints(endpoint =>
{
endpoint.MapSubscribeHandler();
});
流水账式的服务会使得
Program.cs
变得十分臃肿, 可以通过Masa Framework
提供的MinimalAPIs来简化Program.cs
点击查看详情。
总结
我们上面的例子是通过事件总线来完成解耦以及数据模型的同步, 使用的双数据库模式, 但读库使用的是 缓存数据库
, 在Command
端做商品的添加操作, 在Query
端只做查询, 且两端分别使用各自的数据源, 两者业务互不影响, 并且由于缓存数据库性能更强, 它将最大限度的提升性能, 使得我们有更好的使用体验。
在Masa Framework
中仅仅是通过ICommand
、IQuery
将读写分开, 但这并没有硬性要求, 事实上你使用IEvent
也是可以的, CQRS
只是一种设计模式, 这点我们要清楚, 它只是告诉我们要按照一个什么样的标准去做, 但具体怎么来做, 取决于业务的决策者, 除此之外, 后续Masa Framework
还会增加对Event Sourcing
(事件溯源)的支持, 通过事件重放, 允许我们随时重建到对象的任何状态
本章源码
Assignment15
https://github.com/zhenlei520/MasaFramework.Practice
CQRS架构项目:https://github.com/masalabs/MASA.EShop/tree/main/src/Services/Masa.EShop.Services.Catalog
参考
开源地址
MASA.Framework:https://github.com/masastack/MASA.Framework
MASA.EShop:https://github.com/masalabs/MASA.EShop
MASA.Blazor:https://github.com/BlazorComponent/MASA.Blazor
如果你对我们的 MASA Framework 感兴趣, 无论是代码贡献、使用、提 Issue, 欢迎联系我们
命令查询职责分离 - CQRS的更多相关文章
- 架构模式: 命令查询职责分离 (CQRS)
架构模式: 命令查询职责分离 (CQRS) 问题 如何在微服务架构中实现查询 结论 将应用程序拆分为两部分:命令端和查询端.命令端处理创建,更新和删除请求,并在数据更改时发出事件.查询端通过对一个或多 ...
- 浅谈命令查询职责分离(CQRS)模式
在常用的三层架构中,通常都是通过数据访问层来修改或者查询数据,一般修改和查询使用的是相同的实体.在一些业务逻辑简单的系统中可能没有什么问题,但是随着系统逻辑变得复杂,用户增多,这种设计就会出现一些性能 ...
- 转:浅谈命令查询职责分离(CQRS)模式
原文来自于:http://www.cnblogs.com/yangecnu/p/Introduction-CQRS.html 在常用的三层架构中,通常都是通过数据访问层来修改或者查询数据,一般修改和查 ...
- 浅谈命令查询职责分离(CQRS)模式---转载
在常用的三层架构中,通常都是通过数据访问层来修改或者查询数据,一般修改和查询使用的是相同的实体.在一些业务逻辑简单的系统中可能没有什么问题,但是随着系统逻辑变得复杂,用户增多,这种设计就会出现一些性能 ...
- 【转】浅谈命令查询职责分离(CQRS)模式
原文链接:https://www.cnblogs.com/yangecnu/p/Introduction-CQRS.html 在常用的三层架构中,通常都是通过数据访问层来修改或者查询数据,一般修改和查 ...
- 命令查询职责分离(CQRS)模式
参考: http://www.cnblogs.com/yangecnu/p/Introduction-CQRS.html
- IDDD 实现领域驱动设计-CQRS(命令查询职责分离)和 EDA(事件驱动架构)
上一篇:<IDDD 实现领域驱动设计-SOA.REST 和六边形架构> 阅读目录: CQRS-命令查询职责分离 EDA-事件驱动架构 Domin Event-领域事件 Long-Runni ...
- CQRS(命令查询职责分离)和 EDA(事件驱动架构)
转载CQRS(命令查询职责分离)和 EDA(事件驱动架构) 上一篇:<IDDD 实现领域驱动设计-SOA.REST 和六边形架构> 阅读目录: CQRS-命令查询职责分离 EDA-事件驱动 ...
- 命令查询职责分离模式(Command Query Responsibility Segregation,CQRS)
浅谈命令查询职责分离(CQRS)模式 CQRS架构简介 对CQRS的一次批判性思考
- 查询职责分离(CQRS)模式
查询职责分离(CQRS)模式 在常用的三层架构中,通常都是通过数据访问层来修改或者查询数据,一般修改和查询使用的是相同的实体.在一些业务逻辑简单的系统中可能没有什么问题,但是随着系统逻辑变得复杂,用户 ...
随机推荐
- Windows 10 索引设置
有时候想找一下电脑上的某个文件,但是只记得关键字不记得文件名的信息了.这个时候就会尝试在Windows的窗口中搜索.不过有时候明明文件存在,但是无法找到文件.这个时候就需要检查索引设置了.https: ...
- 基于Ubunru服务器搭建wordpress个人博客
一.环境 服务器:阿里云突发性能实例 t5-1核(vCPU) 512 MB + 网络按流量收费(该服务器适用于小型网站) 系统:Ubuntu 22.04 64位Ubuntu 22.04 64位 二. ...
- 基于electron+vue+element构建项目模板之【创建项目篇】
1.概述 electron:使用javascript.css.html构建跨平台的桌面应用程序 vue:数据驱动视图中的一款渐进式的javascript框架 element:基于vue的桌面端UI组件 ...
- centos7使用yum方式安装MySQL5.7
yum -y localinstall http://mirrors.ustc.edu.cn/mysql-repo/mysql57-community-release-el7.rpm yum inst ...
- DevExpress弹框、右键菜单、Grid的使用
很重要!!!Dev为了区分winform的命名,会把一些新添加的属性放在Properties对象里!!找不到想要的属性,记得到里面找找哦! 一.下拉框 在这里假设我们的数据源是db.List(),在这 ...
- 关于使用git传输文件到GitHub
git status(查看本地git仓库情况) git add "文件名(精确到文件拓展名)" git commit -m "说明"(提交到上传缓存区域) gi ...
- 虚拟线程 - VirtualThread源码透视
前提 JDK19于2022-09-20发布GA版本,该版本提供了虚拟线程的预览功能.下载JDK19之后翻看了一下有关虚拟线程的一些源码,跟早些时候的Loom项目构建版本基本并没有很大出入,也跟第三方J ...
- BZOJ3732 Network(Kruskal重构树)
Kruskal重构树的模板题. 给你N个点的无向图 (1 <= N <= 15,000),记为:1-N.图中有M条边 (1 <= M <= 30,000) ,第j条边的长度为: ...
- 利用POI遍历出层级结构的excel表格
import java.util.ArrayList; import java.util.List; import org.apache.poi.ss.util.CellRangeAddress; p ...
- 13.MongoDB系列之分片简介
1. 分片概念 分片是指跨机器拆分数据的过程,有时也会用术语分区.MongoDB既可以手工分片,也支持自动分片 2. 理解集群组件 分片的目标之一是由多个分片组成的集群对应用程序来说就像是一台服务器. ...