eShopOnContainers 知多少[8]:Ordering microservice
1. 引言
Ordering microservice(訂單微服務(wù))就是處理訂單的了,它與前面講到的幾個微服務(wù)相比要復(fù)雜的多。主要涉及以下業(yè)務(wù)邏輯:
- 訂單的創(chuàng)建、取消、支付、發(fā)貨
- 庫存的扣減
2. 架構(gòu)模式

如上圖所示,該服務(wù)基于CQRS 和DDD來實現(xiàn)。

從項目結(jié)構(gòu)來看,主要包括7個項目:
- Ordering.API:應(yīng)用層
- Ordering.Domain:領(lǐng)域?qū)?/li>
- Ordering.Infrastructure:基礎(chǔ)設(shè)施層
- Ordering.BackgroundTasks:后臺任務(wù)
- Ordering.SignalrHub:基于Signalr的消息推送和實時通信
- Ordering.FunctionalTests:功能測試項目
- Ordering.UnitTests:單元測試項目
從以上的項目定義來看,該微服務(wù)的設(shè)計并符合DDD經(jīng)典的四層架構(gòu)。

核心技術(shù)選型:
- ASP.NET Core Web API
- Entity Framework Core
- SQL Server
- Swashbuckle(可選)
- Autofac
- Eventbus
- MediatR
- SignalR
- Dapper
- Polly
- FluentValidator
3. 簡明DDD
領(lǐng)域驅(qū)動設(shè)計是一種方法論,用于解決軟件復(fù)雜度問題。它強調(diào)以領(lǐng)域為核心驅(qū)動設(shè)計。主要包括戰(zhàn)略和戰(zhàn)術(shù)設(shè)計兩大部分,其中戰(zhàn)略設(shè)計指導(dǎo)我們在宏觀層面對問題域進行識別和劃分,從而將大問題劃分為多個小問題,分而治之。而戰(zhàn)術(shù)設(shè)計從微觀層面指導(dǎo)我們?nèi)绾螌︻I(lǐng)域進行建模。

其中戰(zhàn)術(shù)設(shè)計了引入了很多核心要素,指導(dǎo)我們建模:
- 值對象(Value Object)
- 實體(Entity)
- 領(lǐng)域服務(wù)(Domain Service)
- 領(lǐng)域事件(Domain Event)
- 資源庫(Repository)
- 工廠(Factory)
- 聚合(Aggregate)
- 應(yīng)用服務(wù)(Application Service)

其中實體、值對象和領(lǐng)域服務(wù)用于表示領(lǐng)域模型,來實現(xiàn)領(lǐng)域邏輯。
聚合用于封裝一到多個實體和值對象,確保業(yè)務(wù)完整性。
領(lǐng)域事件來豐富領(lǐng)域?qū)ο笾g的交互。
工廠、資源庫用于管理領(lǐng)域?qū)ο蟮纳芷凇?br>
應(yīng)用服務(wù)是用來表達用例和用戶故事。
有了以上的戰(zhàn)術(shù)設(shè)計要素還不夠,如果它們糅合在一起,還是會很混亂,因此DDD再通過分層架構(gòu)來確保關(guān)注點分離,即將領(lǐng)域模型相關(guān)(實體、值對象、聚合、領(lǐng)域服務(wù)、領(lǐng)域事件)放到領(lǐng)域?qū)樱瑢①Y源庫、工廠放到基礎(chǔ)設(shè)施層,將應(yīng)用服務(wù)放到應(yīng)用層。以下就是DDD經(jīng)典的四層架構(gòu):

以上相關(guān)圖片來源于:張逸 · 領(lǐng)域驅(qū)動戰(zhàn)略設(shè)計實踐
如果對訂單微服務(wù)應(yīng)用DDD,那么要摒棄傳統(tǒng)的面向數(shù)據(jù)庫建模的思想,轉(zhuǎn)向領(lǐng)域建模。該項目中主要定義了以下領(lǐng)域?qū)ο螅?/p>
- Order:訂單
- OrderItem:訂單項
- OrderStatus:訂單狀態(tài)
- Buyer:買家
- Address:地址
- PaymentMethod:支付方式
- CardType:銀行卡片類型
在該示例項目中,定義了兩個聚合:訂單聚合和買家聚合,其中Order和Buyer分屬兩個聚合根,其中訂單聚合通過持有買家聚合的唯一ID進行關(guān)聯(lián)。如下圖所示:

我們依次來看其對實體、值對象、聚合、資源庫、領(lǐng)域事件的實現(xiàn)方式。
4.1. 實體、值對象與聚合

實體與值對象最大的區(qū)別在于,實體有標識符可變,值對象不可變。為了保證領(lǐng)域的不變性,也就是更好的封裝,所有的屬性字段都設(shè)置為private set,集合都設(shè)置為只讀的,通過構(gòu)造函數(shù)進行初始化,通過暴露方法供外部調(diào)用修改。
從類圖中我們可以看出,其主要定義了一個Entity抽象基類,所有的實體通過繼承Entity來實現(xiàn)命名約定。這里面有兩點需要說明:
- 通過
Id屬性確保唯一標識符 - 重寫
Equals和GetHashCode方法(hash值計算:this.Id.GetHashCode() ^ 31) - 定義
DomainEvents來存儲實體關(guān)聯(lián)的領(lǐng)域事件(領(lǐng)域事件的發(fā)生歸根結(jié)底是由于領(lǐng)域?qū)ο蟮臓顟B(tài)變化引起的,而領(lǐng)域?qū)ο骩實體、值對象和聚合])中值對象是不可變的,而聚合往往包含多個實體,所以將領(lǐng)域事件關(guān)聯(lián)在實體上最合適不過。)

同樣,值對象也是通過繼承抽象基類ValueObject來進行約定。其主要也是重載了Equals和GetHashCode和方法。這里面有必要學(xué)習(xí)其GetHashCode的實現(xiàn)技巧:
// ValueObject.cs
protected abstract IEnumerable<object> GetAtomicValues();
public override int GetHashCode()
{
return GetAtomicValues()
.Select(x => x != null ? x.GetHashCode() : 0)
.Aggregate((x, y) => x ^ y);
}
//Address.cs
protected override IEnumerable<object> GetAtomicValues()
{
// Using a yield return statement to return each ele
yield return Street;
yield return City;
yield return State;
yield return Country;
yield return ZipCode;
}
可以看到,通過在基類定義GetAtomicValues方法,用來要求子類指定需要hash的字段,然后將每個字段取hash值,然后通過異或運算再行聚合得到唯一hash值。
所有對聚合中領(lǐng)域?qū)ο蟮牟僮鞫际峭ㄟ^聚合根來維護的。因此我們可以看到聚合根中定義了許多方法來處理領(lǐng)域邏輯。
4.2. 倉儲

聚合中的領(lǐng)域?qū)ο蟮某志没柚鷤}儲來完成的。其提供統(tǒng)一的入口來進行聚合內(nèi)相關(guān)領(lǐng)域?qū)ο蟮腃RUD,從而完成透明持久化。從圖中看出,IRepository定義了一個IUnitOfWork屬性,其代表工作單元,主要定義了兩個方法SaveChangesAsync和SaveEntitiesAsync,借助事務(wù)一次性提交所有更改,以確保數(shù)據(jù)的完整性和有效性。
4.3. 領(lǐng)域事件

從類圖中可以看出一個共同特征,都實現(xiàn)了INotification接口。對MediatR熟悉的肯定一眼就明白了。是的,這個是MediatR中定義的接口。借助MediatR,來實現(xiàn)事件處理管道。通過進程內(nèi)事件處理管道來驅(qū)動命令接收,并將它們(在內(nèi)存中)路由到正確的事件處理器。
關(guān)于MeidatR可以參考我的這篇博文:MediatR 知多少
而關(guān)于領(lǐng)域事件的處理,是通過繼承INotificationHanlder接口來實現(xiàn),這樣INotification與INotificationHandler通過Ioc容器的服務(wù)注冊,自動完成事件的訂閱。而領(lǐng)域事件的處理其下放到了Ordering.Api中處理了。這里大家可能會有疑惑,既然叫領(lǐng)域事件,那為什么領(lǐng)域事件的處理不放到領(lǐng)域?qū)幽兀课覀兛梢赃@樣理解,事件是領(lǐng)域內(nèi)觸發(fā),但對事件的處理,其并非都是業(yè)務(wù)邏輯的相關(guān)處理,比如訂單創(chuàng)建成功后發(fā)送短信、郵件等就不屬于業(yè)務(wù)邏輯。
eShopOnContainers中領(lǐng)域事件的觸發(fā)時機并非是即時觸發(fā),選擇的是延遲觸發(fā)模式。具體的實現(xiàn),后面會講到。
5. Ordering.Infrastructure:基礎(chǔ)設(shè)施層
基礎(chǔ)設(shè)施層主要用于提供基礎(chǔ)服務(wù),主要是用來實體映射和持久化。

從圖中可以看到,主要包含以下業(yè)務(wù)處理:
- 實體類型映射
- 冪等性控制器的實現(xiàn)
- 倉儲的具體實現(xiàn)
- 數(shù)據(jù)庫上下文的實現(xiàn)(UnitOfWork的實現(xiàn))
- 領(lǐng)域事件的批量派發(fā)
這里著重下第2、4、5點的介紹。
5.1. 冪等性控制器
冪等性是指某個操作多次執(zhí)行但結(jié)果相同,換句話說,多次執(zhí)行操作而不改變結(jié)果。舉例來說:我們在寫預(yù)插腳本時,會添加條件判斷,當表中不存在數(shù)據(jù)時才將數(shù)據(jù)插入到表中。無論重復(fù)運行多少次 SQL 語句,結(jié)果一定是相同的,并且結(jié)果數(shù)據(jù)會包含在表中。
那怎樣確保冪等性呢?一種方式就是確保操作本身的冪等性,比如可以創(chuàng)建一個表示“將產(chǎn)品價格設(shè)置為¥25”而不是“將產(chǎn)品價格增加¥5”的事件。此時可以安全地處理第一條消息,無論處理多少次結(jié)果都一樣,而第二個消息則完全不同。
但是假設(shè)價格是一個時刻在變的,而你當前的操作就是要將產(chǎn)品價格增加¥5怎么辦呢?顯然這個操作是不能重復(fù)執(zhí)行的。那我如何確保當前的操作只執(zhí)行一次呢?
一種簡便的方法就是記錄每次執(zhí)行的操作。該項目中的Idempotency文件夾就是來做這件事的。

從類圖來看很簡單,就是每次發(fā)送事件時生成一個唯一的Guid,然后構(gòu)造一個ClientRequest對象實例持久化到數(shù)據(jù)庫中,每次借助MediatR發(fā)送消息時都去檢測消息是否已經(jīng)發(fā)送。

5.2. UnitOfWork(工作單元的實現(xiàn))

從代碼來看,主要干了兩件事:
- 在提交變更之前,觸發(fā)所有的領(lǐng)域事件
- 批量提交變更
這里需要解釋的一點是,為什么要在持久化之前而不是之后進行領(lǐng)域事件的觸發(fā)呢?
這種觸發(fā)就是延遲觸發(fā),將領(lǐng)域事件的發(fā)布與領(lǐng)域?qū)嶓w的持久化放到一個事務(wù)中來達到一致性。
當然這有利有弊,弊端就是當領(lǐng)域事件的處理非常耗時,很有可能會導(dǎo)致事務(wù)超時,最終導(dǎo)致提交失敗。而避免這一問題,也只有做事務(wù)拆分,這時就要考慮最終一致性和相應(yīng)的補償措施,顯然更復(fù)雜。
至此,我們可以總結(jié)下聚合、倉儲與數(shù)據(jù)庫之間的關(guān)系,如下圖所示。

6. Ordering.Api:應(yīng)用層
應(yīng)用層通過應(yīng)用服務(wù)接口來暴露系統(tǒng)的全部功能。在這里主要涉及到:
- 領(lǐng)域事件的處理
- 集成事件的處理
- CQRS的實現(xiàn)
- 服務(wù)注冊
- 認證授權(quán)
- 集成事件的訂閱

6.1. 領(lǐng)域事件和集成事件
對于領(lǐng)域事件和集成事件的處理,我們需要先明白二者的區(qū)別。領(lǐng)域事件是發(fā)生在領(lǐng)域內(nèi)的通信(同步或異步均可),而集成事件是基于多個微服務(wù)(其他限界上下文)甚至外部系統(tǒng)或應(yīng)用間的異步通信。
領(lǐng)域事件是借助于MediatR的INotification 和 INotificationHandler的接口來實現(xiàn)。
其中Application/Behaviors文件夾中是實現(xiàn)MediatR中的IPipelineBehavior接口而定義的請求處理管道。

集成事件的發(fā)布訂閱是借助事件總線來完成的,關(guān)于事件總線之前有文章詳述,這里不再贅述。在此,僅代碼舉例其訂閱方式。
private void ConfigureEventBus(IApplicationBuilder app)
{
var eventBus = app.ApplicationServices.GetRequiredService<BuildingBlocks.EventBus.Abstractions.IEventBus>();
eventBus.Subscribe<UserCheckoutAcceptedIntegrationEvent, IIntegrationEventHandler<UserCheckoutAcceptedIntegrationEvent>>();
// some other code
}
6.2. 基于MediatR實現(xiàn)的CQRS
CQRS(Command Query Responsibility Separation):命令查詢職責(zé)分離。是一種用來實現(xiàn)數(shù)據(jù)模型讀寫分離的架構(gòu)模式。顧名思義,分為兩大職責(zé):
- 命令職責(zé)
- 查詢職責(zé)
其核心思想是:在客戶端就將數(shù)據(jù)的新增修改刪除等動作和查詢進行分離,前者稱為Command,通過Command Bus對領(lǐng)域模型進行操作,而查詢則從另外一條路徑直接對數(shù)據(jù)進行操作,比如報表輸出等。

對于命令職責(zé),其是借助于MediatR充當?shù)腃ommandBus,使用IRequest來定義命令,使用IRequestHandler來定義命令處理程序。我們可以看下CancelOrderCommand和CancelOrderCommandHandler的實現(xiàn)。
public class CancelOrderCommand : IRequest<bool>
{
[DataMember]
public int OrderNumber { get; private set; }
public CancelOrderCommand(int orderNumber)
{
OrderNumber = orderNumber;
}
}
public class CancelOrderCommandHandler : IRequestHandler<CancelOrderCommand, bool>
{
private readonly IOrderRepository _orderRepository;
public CancelOrderCommandHandler(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}
public async Task<bool> Handle(CancelOrderCommand command, CancellationToken cancellationToken)
{
var orderToUpdate = await _orderRepository.GetAsync(command.OrderNumber);
if(orderToUpdate == null)
{
return false;
}
orderToUpdate.SetCancelledStatus();
return await _orderRepository.UnitOfWork.SaveEntitiesAsync();
}
}
以上代碼中,有一點需要指出,就是所有Command中的屬性都定義為private set,通過構(gòu)造函數(shù)進行賦值,以確保Command的不變性。
對于查詢職責(zé),通過定義查詢接口,借助Dapper直接寫SQL語句來完成對數(shù)據(jù)庫的直接讀取。

而對于定義的命令,為了確保每個命令的合法性,通過引入第三方Nuget包FluentValdiation來進行命令的合法性校驗。其代碼也很簡單,參考下圖。

6.3. 服務(wù)注冊
整個訂單微服務(wù)中所有服務(wù)的注冊,都是放到應(yīng)用層來做的,在Ordering.Api\Infrastructure\AutofacModules文件夾下通過繼承Autofac.Module定義了兩個Module來進行服務(wù)注冊:
- ApplicationModule:自定義接口相關(guān)服務(wù)的注冊
- MediatorModule:Mediator相關(guān)接口服務(wù)的注冊
將所有的服務(wù)注冊都放到高層模塊來進行注冊,有點違背關(guān)注點分離,各層應(yīng)該關(guān)注本層的服務(wù)注冊,所以這中實現(xiàn)方式是有待改進的。而具體如何改進,這里給大家提供一個線索,可參考ABP是如何實現(xiàn)進行服務(wù)注冊的分離和整合的。
這里順帶提一下Autofac這個Ioc容器的一個限制,就是所有的服務(wù)注冊必須在程序啟動時完成注冊,不允許運行時動態(tài)注冊。
7. Ordering.BackgroundTasks:后臺任務(wù)
后臺任務(wù),顧名思義,后臺靜默運行的任務(wù),也稱計劃任務(wù)。在.NET Core 中,我們將這些類型的任務(wù)稱為托管服務(wù),因為它們是在主機/應(yīng)用程序/微服務(wù)中托管的服務(wù)/邏輯。請注意,這種情況下托管服務(wù)僅簡單表示具有后臺任務(wù)邏輯類。
那我們?nèi)绾螌崿F(xiàn)托管服務(wù)了,一種簡單的方式就是使用.NET Core 2.0之后版本中提供了一個名為IHostedService的新接口。當然也可以選擇其他的一些后臺任務(wù)框架,比如HangFire、Quartz。

該示例項目就是基于BackgroundService定義的一個后臺任務(wù)。該任務(wù)主要用于輪詢訂單表中處于已提交超過1分鐘的訂單,然后發(fā)布集成事件到事件總線,最終用來將訂單狀態(tài)更新為待核驗(庫存)狀態(tài)。
public abstract class BackgroundService : IHostedService, IDisposable
{
protected BackgroundService();
public virtual void Dispose();
public virtual Task StartAsync(CancellationToken cancellationToken);
[AsyncStateMachine(typeof(<StopAsync>d__4))]
public virtual Task StopAsync(CancellationToken cancellationToken);
protected abstract Task ExecuteAsync(CancellationToken stoppingToken);
}
從BackgroundService的方法申明中我們可以看出僅需實現(xiàn)ExecuteAsync方法即可。
完成后臺任務(wù)的定義后,將服務(wù)注冊到Ioc容器中即可。
public IServiceProvider ConfigureServices(IServiceCollection services)
{
//Other DI registrations;
// Register Hosted Services
services.AddSingleton<IHostedService, GracePeriodManagerService>();
services.AddSingleton<IHostedService, MyHostedServiceB>();
services.AddSingleton<IHostedService, MyHostedServiceC>();
//...
}

總之,IHostedService 接口為 ASP.NET Core Web 應(yīng)用程序啟動后臺任務(wù)提供了一種便捷的方法。它的優(yōu)勢主要在于:當主機本身關(guān)閉時,可以利用取消令牌來優(yōu)雅的清理后臺任務(wù)。
8. Ordering.SignalrHub:即時通信
在訂單微服務(wù)中,當訂單狀態(tài)變更時,需要實時推送訂單狀態(tài)變更消息給客戶端。而這就涉及到實時通信。實時 HTTP 通信意味著,當數(shù)據(jù)可用時,服務(wù)端代碼會推送內(nèi)容到已連接的客戶端,而不是服務(wù)端等待客戶端來請求新數(shù)據(jù)。
而對于實時通信,ASP.NET Core中SignalR可以滿足我們的需求,其支持幾種處理實時通信的技術(shù)以確保實時通信的可靠傳輸。
- WebSockets
- Server-Sent Events
- Long Polling

該示例項目的實現(xiàn)思路很簡單:
- 訂閱訂單狀態(tài)變更相關(guān)的集成事件
- 繼承
SignalR.Hub定義一個NotificationsHub - 在集成事件處理程序中調(diào)用Hub進行消息的實時推送
// 訂閱集成事件
private void ConfigureEventBus(IApplicationBuilder app)
{
var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>();
eventBus.Subscribe<OrderStatusChangedToAwaitingValidationIntegrationEvent, OrderStatusChangedToAwaitingValidationIntegrationEventHandler>();
eventBus.Subscribe<OrderStatusChangedToPaidIntegrationEvent, OrderStatusChangedToPaidIntegrationEventHandler>();
eventBus.Subscribe<OrderStatusChangedToStockConfirmedIntegrationEvent, OrderStatusChangedToStockConfirmedIntegrationEventHandler>();
eventBus.Subscribe<OrderStatusChangedToShippedIntegrationEvent, OrderStatusChangedToShippedIntegrationEventHandler>();
eventBus.Subscribe<OrderStatusChangedToCancelledIntegrationEvent, OrderStatusChangedToCancelledIntegrationEventHandler>();
eventBus.Subscribe<OrderStatusChangedToSubmittedIntegrationEvent, OrderStatusChangedToSubmittedIntegrationEventHandler>();
}
// 定義SignalR.Hub
[Authorize]
public class NotificationsHub : Hub
{
public override async Task OnConnectedAsync()
{
await Groups.AddToGroupAsync(Context.ConnectionId, Context.User.Identity.Name);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception ex)
{
await Groups.AddToGroupAsync(Context.ConnectionId, Context.User.Identity.Name);
await base.OnDisconnectedAsync(ex);
}
}
// 在集成事件處理器中調(diào)用Hub進行消息的實時推送
public class OrderStatusChangedToPaidIntegrationEventHandler : IIntegrationEventHandler<OrderStatusChangedToPaidIntegrationEvent>
{
private readonly IHubContext<NotificationsHub> _hubContext;
public OrderStatusChangedToPaidIntegrationEventHandler(IHubContext<NotificationsHub> hubContext)
{
_hubContext = hubContext ?? throw new ArgumentNullException(nameof(hubContext));
}
public async Task Handle(OrderStatusChangedToPaidIntegrationEvent @event)
{
await _hubContext.Clients
.Group(@event.BuyerName)
.SendAsync("UpdatedOrderState", new { OrderId = @event.OrderId, Status = @event.OrderStatus });
}
}
8. 最后
訂單微服務(wù)在整個eShopOnContainers中屬于最復(fù)雜的一個微服務(wù)了。
通過對DDD的簡要介紹,以及對每一層的技術(shù)選型以及實現(xiàn)的思路和邏輯的梳理,希望對你有所幫助。
推薦鏈接:你必須知道的ML.NET開發(fā)指南
推薦鏈接:你必須知道的Office開發(fā)指南
推薦鏈接:你必須知道的IOT開發(fā)指南
推薦鏈接:你必須知道的Azure基礎(chǔ)知識
推薦鏈接:你必須知道的PowerBI基礎(chǔ)知識
關(guān)注我的公眾號『微服務(wù)知多少』,我們微信不見不散。
閱罷此文,如果您覺得本文不錯并有所收獲,請【打賞】或【推薦】,也可【評論】留下您的問題或建議與我交流。 你的支持是我不斷創(chuàng)作和分享的不竭動力!

浙公網(wǎng)安備 33010602011771號