概念
CQRS
?(https://learn.microsoft.com/zh-cn/azure/architecture/patterns/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的使用固然會帶來很多好處, 但同樣它也會給項目帶來復雜度的提升, 并且雙數據庫模式、事件溯源模式?(https://microservices.io/patterns/data/event-sourcing.html)?的CQRS, 使用的是最終一致性, 這些都是我們在選擇技術方案時必須要考慮的
設計
上述文章中我們了解到了CQRS其本質上是一種讀寫分離的設計思想, 它并不是強制性的規定必須要怎樣去做, 這點與之前的IEvent
?(進程內事件)、IIntegrationEvent
?(跨進程事件)不同, 它并不是強制性的, 根據CQRS
的設計模式我們將事件分成Command
、Query
由于Query
?(查詢) 是需要有返回值的, 因此我們在繼承IEvent
的同時, 還額外增加了一個Result
屬性用以存儲結果, 我們希望將查詢的結果保存到Result
中, 但它不是強制性的, 我們并沒有強制性要求必須要將結果保存到Result
中。
由于Command
?(命令) 是沒有返回值的, 因此我們并沒有額外新增Result
屬性, 我們認為命令會更新數據, 那就需要用到工作單元, 因此Command
除了繼承IEvent
之外, 還繼承了ITransaction
,這方便了我們在Handler
中的可以通過@event.UnitOfWork
來管理工作單元, 而不需要通過構造函數來獲取
但MASA Framework
?
(https://github.com/masastack/MASA.Framework)?并沒有要求必須使用?Event Sourcing 模式
?
(https://microservices.io/patterns/data/event-sourcing.html)?或者?雙數據庫模式
?的CQRS, 具體使用哪種實現, 它取決于業務的決策者
下面就就來看看MASA Framework提供的CQRS
是如何使用的
入門
安裝.NET 6.0?(https://dotnet.microsoft.com/zh-cn/download/dotnet/6.0)
1.新建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.CqrsDemodotnet 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 //使用CQRSdotnet add package Masa.Contrib.Development.DaprStarter.AspNetCore --version 0.7.0-preview.9 //開發環境下協助 Dapr Sidecar, 用于通過Dapr發布集成事件
?2.注冊跨進程事件總線、進程內事件總線, 修改類Program.cs
示例中未真實使用DB, 不再使用發件箱模式, 只需要使用集成事件提供的PubSub
?(https://docs.dapr.io/zh-hans/developing-applications/building-blocks/pubsub/pubsub-overview/)能力即可
builder.Services.AddIntegrationEventBus(dispatcherOptions =>
{dispatcherOptions.UseDapr();//使用 Dapr 提供的PubSub能力dispatcherOptions.UseEventBus();//使用進程內事件總線
});
3.注冊Dapr Starter協助管理Dapr Sidecar
?(開發環境使用)
if (builder.Environment.IsDevelopment())builder.Services.AddDaprStarter();
4.新增加添加商品方法, 修改類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; }
}
5.新增加查詢商品的方法, 修改類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; }
}
6.新增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后并獲取商品idawait 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);
}
7.新增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;}
}
8.新增添加商品的跨進程事件的處理服務, 修改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
?點擊查看詳情(https://blogs.masastack.com/2022/07/12/masa/framework/practice/14.%20%E6%9C%80%E5%B0%8F%20API%20-%20MinimalAPIs)
我們上面的例子是通過事件總線來完成解耦以及數據模型的同步, 使用的雙數據庫模式, 但讀庫使用的是?緩存數據庫
, 在Command
端做商品的添加操作, 在Query
端只做查詢, 且兩端分別使用各自的數據源, 兩者業務互不影響, 并且由于緩存數據庫性能更強, 它將最大限度的提升性能, 使得我們有更好的使用體驗。
在Masa Framework中僅僅是通過ICommand
、IQuery
將讀寫分開, 但這并沒有硬性要求, 事實上你使用IEvent
也是可以的,?CQRS只是一種設計模式, 這點我們要清楚, 它只是告訴我們要按照一個什么樣的標準去做, 但具體怎么來做, 取決于業務的決策者, 除此之外, 后續Masa Framework還會增加對Event Sourcing
(事件溯源?(https://docs.microsoft.com/zh-cn/azure/architecture/patterns/event-sourcing))的支持, 通過事件重放, 允許我們隨時重建到對象的任何狀態
本章源碼
Assignment15
https://github.com/zhenlei520/MasaFramework.Practice
CQRS架構項目:https://github.com/masalabs/MASA.EShop/tree/main/src/Services/Masa.EShop.Services.Catalog
參考
CQRS 模式?
(https://learn.microsoft.com/zh-cn/azure/architecture/patterns/cqrs)
在微服務中應用簡化后的 CQRS 和 DDD 模式
?(https://learn.microsoft.com/zh-cn/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/apply-simplified-microservice-cqrs-ddd-patterns)
.NET現代化應用開發 - CQRS&類目管理代碼剖析?
(https://www.bilibili.com/video/BV1D24y1R7jE)
開源地址
MASA.Framework:https://github.com/masastack/MASA.Framework
如果你對我們的 MASA Framework 感興趣, 無論是代碼貢獻、使用、提 Issue, 歡迎聯系我們
《MASA?Framework實戰課程》已開課
點擊“閱讀原文”查看課程安排