CQRS
是一種與領域驅動設計和事件溯源相關的架構模式, 它的全稱是Command Query Responsibility Segregation, 又叫命令查詢職責分離, Greg Young在2010年創造了這個術語, 它是基於Bertrand Meyer 的 CQS (Command-Query Separation 命令查詢分離原則) 設計模式。
CQRS
認為不論業務多複雜在最終實現的時候, 無非是讀寫操作, 因此建議將應用程式分為兩個方面, 即Command(命令)和Query(查詢)
命令端:
查詢端:
命令與讀取操作的是同一個資料庫, 命令端通過ORM框架將實體儲存到資料庫中, 查詢端通過資料存取層獲取資料 (資料存取層通過ORM框架或者儲存過程獲取資料)
命令與讀取操作的是不同的資料庫, 命令端通過ORM框架將實體儲存到 寫庫 (Write Db), 並將本地改動推播到 讀庫 (Read Db), 查詢端通過資料存取層存取 讀庫 (Read Db), 使用這種模式可以帶來以下好處:
通過事件溯源實現的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
是如何使用的
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 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, 歡迎聯絡我們
本文來自部落格園,作者:磊_磊,轉載請註明原文連結:https://www.cnblogs.com/zhenlei520/p/16921757.html
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連結,否則保留追究法律責任的權利