eShopOnContainers 知多少[8]:Ordering microservice

1. 引言

Ordering microservice(訂單微服務)就是處理訂單的了,它與前面講到的幾個微服務相比要複雜的多。主要涉及如下業務邏輯:html

  1. 訂單的建立、取消、支付、發貨
  2. 庫存的扣減

2. 架構模式

簡化的CQRS和DDD微服務設計

如上圖所示,該服務基於CQRS 和DDD來實現。git

項目結構

從項目結構來看,主要包括7個項目:github

  1. Ordering.API:應用層
  2. Ordering.Domain:領域層
  3. Ordering.Infrastructure:基礎設施層
  4. Ordering.BackgroundTasks:後臺任務
  5. Ordering.SignalrHub:基於Signalr的消息推送和實時通訊
  6. Ordering.FunctionalTests:功能測試項目
  7. Ordering.UnitTests:單元測試項目

從以上的項目定義來看,該微服務的設計並符合DDD經典的四層架構。數據庫

Ordering.API對應DDD中分層

核心技術選型:安全

  1. ASP.NET Core Web API
  2. Entity Framework Core
  3. SQL Server
  4. Swashbuckle(可選)
  5. Autofac
  6. Eventbus
  7. MediatR
  8. SignalR
  9. Dapper
  10. Polly
  11. FluentValidator

3. 簡明DDD

領域驅動設計是一種方法論,用於解決軟件複雜度問題。它強調以領域爲核心驅動設計。主要包括戰略和戰術設計兩大部分,其中戰略設計指導咱們在宏觀層面對問題域進行識別和劃分,從而將大問題劃分爲多個小問題,分而治之。而戰術設計從微觀層面指導咱們如何對領域進行建模。 bash

DDD開發過程
其中戰術設計了引入了不少核心要素,指導咱們建模:

  1. 值對象(Value Object)
  2. 實體(Entity)
  3. 領域服務(Domain Service)
  4. 領域事件(Domain Event)
  5. 資源庫(Repository)
  6. 工廠(Factory)
  7. 聚合(Aggregate)
  8. 應用服務(Application Service)
    戰術要素

其中實體、值對象和領域服務用於表示領域模型,來實現領域邏輯。 聚合用於封裝一到多個實體和值對象,確保業務完整性。 領域事件來豐富領域對象之間的交互。 工廠、資源庫用於管理領域對象的生命週期。 應用服務是用來表達用例和用戶故事。架構

有了以上的戰術設計要素還不夠,若是它們糅合在一塊兒,仍是會很混亂,所以DDD再經過分層架構來確保關注點分離,即將領域模型相關(實體、值對象、聚合、領域服務、領域事件)放到領域層,將資源庫、工廠放到基礎設施層,將應用服務放到應用層。如下就是DDD經典的四層架構: app

DDD經典四層架構

以上相關圖片來源於:張逸 · 領域驅動戰略設計實踐框架

4. Ordering.Domain:領域層

項目結構

若是對訂單微服務應用DDD,那麼要摒棄傳統的面向數據庫建模的思想,轉向領域建模。該項目中主要定義瞭如下領域對象:異步

  • Order:訂單
  • OrderItem:訂單項
  • OrderStatus:訂單狀態
  • Buyer:買家
  • Address:地址
  • PaymentMethod:支付方式
  • CardType:銀行卡片類型

在該示例項目中,定義了兩個聚合:訂單聚合和買家聚合,其中Order和Buyer分屬兩個聚合根,其中訂單聚合經過持有買家聚合的惟一ID進行關聯。以下圖所示:

訂單聚會和買家聚合

咱們依次來看其對實體、值對象、聚合、資源庫、領域事件的實現方式。

4.1. 實體、值對象與聚合

實體相關類圖

實體與值對象最大的區別在於,實體有標識符可變,值對象不可變。爲了保證領域的不變性,也就是更好的封裝,全部的屬性字段都設置爲private set,集合都設置爲只讀的,經過構造函數進行初始化,經過暴露方法供外部調用修改。 從類圖中咱們能夠看出,其主要定義了一個Entity抽象基類,全部的實體經過繼承Entity來實現命名約定。這裏面有兩點須要說明:

  1. 經過Id屬性確保惟一標識符
  2. 重寫EqualsGetHashCode方法(hash值計算:this.Id.GetHashCode() ^ 31)
  3. 定義DomainEvents來存儲實體關聯的領域事件(領域事件的發生歸根結底是因爲領域對象的狀態變化引發的,而領域對象[實體、值對象和聚合])中值對象是不可變的,而聚合每每包含多個實體,因此將領域事件關聯在實體上最合適不過。)

值對象相關類圖

一樣,值對象也是經過繼承抽象基類ValueObject來進行約定。其主要也是重載了EqualsGetHashCode和方法。這裏面有必要學習其GetHashCode的實現技巧:

// 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值。

全部對聚合中領域對象的操做都是經過聚合根來維護的。所以咱們能夠看到聚合根中定義了許多方法來處理領域邏輯。

4.2. 倉儲

倉儲相關類圖
聚合中的領域對象的持久化藉助倉儲來完成的。其提供統一的入口來進行聚合內相關領域對象的CRUD,從而完成透明持久化。從圖中看出, IRepository定義了一個 IUnitOfWork屬性,其表明工做單元,主要定義了兩個方法 SaveChangesAsyncSaveEntitiesAsync,藉助事務一次性提交全部更改,以確保數據的完整性和有效性。

4.3. 領域事件

領域事件相關類圖

從類圖中能夠看出一個共同特徵,都實現了INotification接口。對MediatR熟悉的確定一眼就明白了。是的,這個是MediatR中定義的接口。藉助MediatR,來實現事件處理管道。經過進程內事件處理管道來驅動命令接收,並將它們(在內存中)路由到正確的事件處理器。 關於MeidatR能夠參考個人這篇博文:MediatR 知多少

而關於領域事件的處理,是經過繼承INotificationHanlder接口來實現,這樣INotificationINotificationHandler經過Ioc容器的服務註冊,自動完成事件的訂閱。而領域事件的處理其下放到了Ordering.Api中處理了。這裏你們可能會有疑惑,既然叫領域事件,那爲何領域事件的處理不放到領域層呢?咱們能夠這樣理解,事件是領域內觸發,但對事件的處理,其並不是都是業務邏輯的相關處理,好比訂單建立成功後發送短信、郵件等就不屬於業務邏輯。

eShopOnContainers中領域事件的觸發時機並不是是即時觸發,選擇的是延遲觸發模式。具體的實現,後面會講到。

5. Ordering.Infrastructure:基礎設施層

基礎設施層主要用於提供基礎服務,主要是用來實體映射和持久化。

Ordering.Infrastructure 代碼結構

從圖中能夠看到,主要包含如下業務處理:

  1. 實體類型映射
  2. 冪等性控制器的實現
  3. 倉儲的具體實現
  4. 數據庫上下文的實現(UnitOfWork的實現)
  5. 領域事件的批量派發

這裏着重下第二、四、5點的介紹。

5.1. 冪等性控制器

冪等性是指某個操做屢次執行但結果相同,換句話說,屢次執行操做而不改變結果。舉例來講:咱們在寫預插腳本時,會添加條件判斷,當表中不存在數據時纔將數據插入到表中。不管重複運行多少次 SQL 語句,結果必定是相同的,而且結果數據會包含在表中。

那怎樣確保冪等性呢?一種方式就是確保操做自己的冪等性,好比能夠建立一個表示「將產品價格設置爲¥25」而不是「將產品價格增長¥5」的事件。此時能夠安全地處理第一條消息,不管處理多少次結果都同樣,而第二個消息則徹底不一樣。 可是假設價格是一個時刻在變的,而你當前的操做就是要將產品價格增長¥5怎麼辦呢?顯然這個操做是不能重複執行的。那我如何確保當前的操做只執行一次呢? 一種簡便的方法就是記錄每次執行的操做。該項目中的Idempotency文件夾就是來作這件事的。

Idempotency 類圖

從類圖來看很簡單,就是每次發送事件時生成一個惟一的Guid,而後構造一個ClientRequest對象實例持久化到數據庫中,每次藉助MediatR發送消息時都去檢測消息是否已經發送。

冪等性處理

5.2. UnitOfWork(工做單元的實現)

Uow實現邏輯

從代碼來看,主要乾了兩件事:

  1. 在提交變動以前,觸發全部的領域事件
  2. 批量提交變動

這裏須要解釋的一點是,爲何要在持久化以前而不是以後進行領域事件的觸發呢? 這種觸發就是延遲觸發,將領域事件的發佈與領域實體的持久化放到一個事務中來達到一致性。 固然這有利有弊,弊端就是當領域事件的處理很是耗時,頗有可能會致使事務超時,最終致使提交失敗。而避免這一問題,也只有作事務拆分,這時就要考慮最終一致性和相應的補償措施,顯然更復雜。

至此,咱們能夠總結下聚合、倉儲與數據庫之間的關係,以下圖所示。

6. Ordering.Api:應用層

應用層經過應用服務接口來暴露系統的所有功能。在這裏主要涉及到:

  1. 領域事件的處理
  2. 集成事件的處理
  3. CQRS的實現
  4. 服務註冊
  5. 認證受權
  6. 集成事件的訂閱

項目結構

6.1. 領域事件和集成事件

對於領域事件和集成事件的處理,咱們須要先明白兩者的區別。領域事件是發生在領域內的通訊(同步或異步都可),而集成事件是基於多個微服務(其餘限界上下文)甚至外部系統或應用間的異步通訊。 領域事件是藉助於MediatR的INotification 和 INotificationHandler的接口來實現。

其中Application/Behaviors文件夾中是實現MediatR中的IPipelineBehavior接口而定義的請求處理管道。

集成事件的發佈訂閱是藉助事件總線來完成的,關於事件總線以前有文章詳述,這裏再也不贅述。在此,僅代碼舉例其訂閱方式。

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實現的CQRS

CQRS(Command Query Responsibility Separation):命令查詢職責分離。是一種用來實現數據模型讀寫分離的架構模式。顧名思義,分爲兩大職責:

  1. 命令職責
  2. 查詢職責

其核心思想是:在客戶端就將數據的新增修改刪除等動做和查詢進行分離,前者稱爲Command,經過Command Bus對領域模型進行操做,而查詢則從另一條路徑直接對數據進行操做,好比報表輸出等。

CQRS

對於命令職責,其是藉助於MediatR充當的CommandBus,使用IRequest來定義命令,使用IRequestHandler來定義命令處理程序。咱們能夠看下CancelOrderCommandCancelOrderCommandHandler的實現。

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,經過構造函數進行賦值,以確保Command的不變性。

對於查詢職責,經過定義查詢接口,藉助Dapper直接寫SQL語句來完成對數據庫的直接讀取。

查詢示例

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

校驗器的定義和註冊

6.3. 服務註冊

整個訂單微服務中全部服務的註冊,都是放到應用層來作的,在Ordering.Api\Infrastructure\AutofacModules文件夾下經過繼承Autofac.Module定義了兩個Module來進行服務註冊:

  • ApplicationModule:自定義接口相關服務的註冊
  • MediatorModule:Mediator相關接口服務的註冊

將全部的服務註冊都放到高層模塊來進行註冊,有點違背關注點分離,各層應該關注本層的服務註冊,因此這中實現方式是有待改進的。而具體如何改進,這裏給你們提供一個線索,可參考ABP是如何實現進行服務註冊的分離和整合的。

這裏順帶提一下Autofac這個Ioc容器的一個限制,就是全部的服務註冊必須在程序啓動時完成註冊,不容許運行時動態註冊。

7. Ordering.BackgroundTasks:後臺任務

後臺任務,顧名思義,後臺靜默運行的任務,也稱計劃任務。在.NET Core 中,咱們將這些類型的任務稱爲託管服務,由於它們是在主機/應用程序/微服務中託管的服務/邏輯。請注意,這種狀況下託管服務僅簡單表示具備後臺任務邏輯類。

那咱們如何實現託管服務了,一種簡單的方式就是使用.NET Core 2.0以後版本中提供了一個名爲IHostedService的新接口。固然也能夠選擇其餘的一些後臺任務框架,好比HangFire、Quartz。

該示例項目就是基於BackgroundService定義的一個後臺任務。該任務主要用於輪詢訂單表中處於已提交超過1分鐘的訂單,而後發佈集成事件到事件總線,最終用來將訂單狀態更新爲待覈驗(庫存)狀態。

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的方法申明中咱們能夠看出僅需實現ExecuteAsync方法便可。

完成後臺任務的定義後,將服務註冊到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 應用程序啓動後臺任務提供了一種便捷的方法。它的優點主要在於:當主機自己關閉時,能夠利用取消令牌來優雅的清理後臺任務。

8. Ordering.SignalrHub:即時通訊

在訂單微服務中,當訂單狀態變動時,須要實時推送訂單狀態變動消息給客戶端。而這就涉及到實時通訊。實時 HTTP 通訊意味着,當數據可用時,服務端代碼會推送內容到已鏈接的客戶端,而不是服務端等待客戶端來請求新數據。

而對於實時通訊,ASP.NET Core中SignalR能夠知足咱們的需求,其支持幾種處理實時通訊的技術以確保實時通訊的可靠傳輸。

該示例項目的實現思路很簡單:

  1. 訂閱訂單狀態變動相關的集成事件
  2. 繼承SignalR.Hub定義一個NotificationsHub
  3. 在集成事件處理程序中調用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);
    }
}

// 在集成事件處理器中調用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. 最後

訂單微服務在整個eShopOnContainers中屬於最複雜的一個微服務了。 經過對DDD的簡要介紹,以及對每一層的技術選型以及實現的思路和邏輯的梳理,但願對你有所幫助。

相關文章
相關標籤/搜索