輕鬆應對併發,Newbe.Claptrap 框架入門,第四步 —— 利用 Minion,商品下單

Newbe36524發表於2020-08-27

接上一篇 Newbe.Claptrap 框架入門,第三步 —— 定義 Claptrap,管理商品庫存 ,我們繼續要了解一下如何使用 Newbe.Claptrap 框架開發業務。通過本篇閱讀,您便可以開始學會在 Claptrap 框架中使用 Minion 進行非同步的業務處理。

Newbe.Claptrap 是一個用於輕鬆應對併發問題的分散式開發框架。如果您是首次閱讀本系列文章。建議可以先從本文末尾的入門文章開始瞭解。

開篇摘要

本篇,我通過實現 “商品下單” 的需求來了解一下如何在已有的專案樣例中使用 Minion 來完成非同步的業務處理。

首先,先了解一下本篇需要涉及的業務用例:

  1. 使用者可以進行下單操作,下單時將使用當前購物車中的所有 SKU 形成一個訂單。
  2. 下單後將會扣除相關 SKU 的庫存。如果某一 SKU 庫存不足,則下單失敗。
  3. 下單操作僅到扣減庫存成功為止,後續步驟不需要本樣例討論範圍。因此,本樣例在成功下單之後會在資料庫中生成一條訂單記錄,表示訂單建立結束。

本篇雖然重點在於 Minion 的使用,不過由於需要使用到一個新的 OrderGrain 物件,因此還是需要使用到前一篇 “定義 Claptrap” 的相關知識。

Minion 是一種特殊的 Claptrap,它與其 MasterClaptrap 之間的關係如下圖所示:

Minion

其主體開發流程和 Claptrap 類似,只是有所刪減。對比如下:

步驟ClaptrapMinion
定義 ClaptrapTypeCode
定義 State
定義 Grain 介面
實現 Grain
註冊 Grain
定義 EventCode  
定義 Event  
實現 EventHandler
註冊 EventHandler
實現 IInitialStateDataFactory

這個刪減的原因是由於 Minion 是 Claptrap 的事件消費者,所以事件相關的定義不需要處理。但是其他的部分仍然是必須的。

本篇開始,我們將不再羅列相關程式碼所在的具體檔案位置,希望讀者能夠自行在專案中進行查詢,以便熟練的掌握。

實現 OrderGrain

基於前一篇 “定義 Claptrap” 相關的知識,我們此處實現一個 OrderGrain 用來表示訂單下單操作。為節約篇幅,我們只羅列其中關鍵的部分。

OrderState

訂單狀態的定義如下:

using System.Collections.Generic;
using Newbe.Claptrap;

namespace HelloClaptrap.Models.Order
{
    public class OrderState : IStateData
    {
        public bool OrderCreated { get; set; }
        public string UserId { get; set; }
        public Dictionary<string, int> Skus { get; set; }
    }
}

 

 
  1. OrderCreated 表示訂單是否已經建立,避免重複建立訂單
  2. UserId 下單使用者 Id
  3. Skus 訂單包含的 SkuId 和訂單量

OrderCreatedEvent

訂單建立事件的定義如下:

using System.Collections.Generic;
using Newbe.Claptrap;

namespace HelloClaptrap.Models.Order.Events
{
    public class OrderCreatedEvent : IEventData
    {
        public string UserId { get; set; }
        public Dictionary<string, int> Skus { get; set; }
    }
}

 

OrderGrain

using System.Threading.Tasks;
using HelloClaptrap.Actors.Order.Events;
using HelloClaptrap.IActor;
using HelloClaptrap.Models;
using HelloClaptrap.Models.Order;
using HelloClaptrap.Models.Order.Events;
using Newbe.Claptrap;
using Newbe.Claptrap.Orleans;
using Orleans;

namespace HelloClaptrap.Actors.Order
{
    [ClaptrapEventHandler(typeof(OrderCreatedEventHandler), ClaptrapCodes.OrderCreated)]
    public class OrderGrain : ClaptrapBoxGrain<OrderState>, IOrderGrain
    {
        private readonly IGrainFactory _grainFactory;

        public OrderGrain(IClaptrapGrainCommonService claptrapGrainCommonService,
            IGrainFactory grainFactory)
            : base(claptrapGrainCommonService)
        {
            _grainFactory = grainFactory;
        }

        public async Task CreateOrderAsync(CreateOrderInput input)
        {
            var orderId = Claptrap.State.Identity.Id;
            // throw exception if order already created
            if (StateData.OrderCreated)
            {
                throw new BizException($"order with order id already created : {orderId}");
            }

            // get items from cart
            var cartGrain = _grainFactory.GetGrain<ICartGrain>(input.CartId);
            var items = await cartGrain.GetItemsAsync();

            // update inventory for each sku
            foreach (var (skuId, count) in items)
            {
                var skuGrain = _grainFactory.GetGrain<ISkuGrain>(skuId);
                await skuGrain.UpdateInventoryAsync(-count);
            }

            // remove all items from cart
            await cartGrain.RemoveAllItemsAsync();

            // create a order
            var evt = this.CreateEvent(new OrderCreatedEvent
            {
                UserId = input.UserId,
                Skus = items
            });
            await Claptrap.HandleEventAsync(evt);
        }
    }
}

 

  1. OrderGrain 實現訂單的建立核心邏輯,其中的 CreateOrderAsync 方法完成購物車資料獲取,庫存扣減相關的動作。
  2. OrderCreatedEvent 執行成功後將會更新 State 中相關的欄位,此處就不在列出了。

通過 Minion 向資料庫儲存訂單資料

從系列開頭到此,我們從未提及資料庫相關的操作。因為當您在使用 Claptrap 框架時,絕大多數的操作都已經被 “事件的寫入” 和 “狀態的更新” 代替了,故而完全不需要親自編寫資料庫操作。

不過,由於 Claptrap 通常是對應單體物件(一個訂單,一個 SKU,一個購物車)而設計的,因而無法獲取全體(所有訂單,所有 SKU,所有購物車)的資料情況。此時,就需要將狀態資料持久化到另外的持久化結構中(資料庫,檔案,快取等)以便完成全體情況的查詢或其他操作。

在 Claptrap 框架中引入了 Minion 的概念來解決上述的需求。

接下來,我們就在樣例中引入一個 OrderDbGrain (一個 Minion)來非同步完成 OrderGrain 的訂單入庫操作。

定義 ClaptrapTypeCode

  namespace HelloClaptrap.Models
  {
      public static class ClaptrapCodes
      {
          #region Cart

          public const string CartGrain = "cart_claptrap_newbe";
          private const string CartEventSuffix = "_e_" + CartGrain;
          public const string AddItemToCart = "addItem" + CartEventSuffix;
          public const string RemoveItemFromCart = "removeItem" + CartEventSuffix;
          public const string RemoveAllItemsFromCart = "remoeAllItems" + CartEventSuffix;

          #endregion

          #region Sku

          public const string SkuGrain = "sku_claptrap_newbe";
          private const string SkuEventSuffix = "_e_" + SkuGrain;
          public const string SkuInventoryUpdate = "inventoryUpdate" + SkuEventSuffix;

          #endregion

          #region Order

          public const string OrderGrain = "order_claptrap_newbe";
          private const string OrderEventSuffix = "_e_" + OrderGrain;
          public const string OrderCreated = "orderCreated" + OrderEventSuffix;

+         public const string OrderDbGrain = "db_order_claptrap_newbe";

          #endregion
      }
  }

 

Minion 是一種特殊的 Claptrap,換言之,它也是一種 Claptrap。而 ClaptrapTypeCode 對於 Claptrap 來說是必需的,因而需要增加此定義。

定義 State

由於本樣例只需要向資料庫寫入一條訂單記錄就可以了,並不需要在 State 中任何資料,因此該步驟在本樣例中其實並不需要。

定義 Grain 介面

+ using HelloClaptrap.Models;
+ using Newbe.Claptrap;
+ using Newbe.Claptrap.Orleans;
+
+ namespace HelloClaptrap.IActor
+ {
+     [ClaptrapMinion(ClaptrapCodes.OrderGrain)]
+     [ClaptrapState(typeof(NoneStateData), ClaptrapCodes.OrderDbGrain)]
+     public interface IOrderDbGrain : IClaptrapMinionGrain
+     {
+     }
+ }

 

  1. ClaptrapMinion 用來標記該 Grain 是一個 Minion,其中的 Code 指向其對應的 MasterClaptrap。
  2. ClaptrapState 用來標記 Claptrap 的 State 資料型別。前一步,我們闡明該 Minion 並不需要 StateData,因此使用 NoneStateData 這一框架內建型別來代替。
  3. IClaptrapMinionGrain 是區別於 IClaptrapGrain 的 Minion 介面。如果一個 Grain 是 Minion ,則需要繼承該介面。
  4. ClaptrapCodes.OrderGrain 和 ClaptrapCodes.OrderDbGrain 是兩個不同的字串,希望讀者不是星際宗師。

星際宗師:因為星際爭霸比賽節奏快,資訊量大,選手很容易忽視或誤判部分資訊,因此經常發生 “選手看不到發生在眼皮底下的關鍵事件” 的搞笑失誤。玩家們由此調侃星際玩家都是瞎子(曾經真的有一場盲人和職業選手的對決),段位越高,瞎得越嚴重,職業星際選手清一色的盲人。

實現 Grain

+ using System.Collections.Generic;
+ using System.Threading.Tasks;
+ using HelloClaptrap.Actors.DbGrains.Order.Events;
+ using HelloClaptrap.IActor;
+ using HelloClaptrap.Models;
+ using Newbe.Claptrap;
+ using Newbe.Claptrap.Orleans;
+
+ namespace HelloClaptrap.Actors.DbGrains.Order
+ {
+     [ClaptrapEventHandler(typeof(OrderCreatedEventHandler), ClaptrapCodes.OrderCreated)]
+     public class OrderDbGrain : ClaptrapBoxGrain<NoneStateData>, IOrderDbGrain
+     {
+         public OrderDbGrain(IClaptrapGrainCommonService claptrapGrainCommonService)
+             : base(claptrapGrainCommonService)
+         {
+         }
+
+         public async Task MasterEventReceivedAsync(IEnumerable<IEvent> events)
+         {
+             foreach (var @event in events)
+             {
+                 await Claptrap.HandleEventAsync(@event);
+             }
+         }
+
+         public Task WakeAsync()
+         {
+             return Task.CompletedTask;
+         }
+     }
+ }

 

  1. MasterEventReceivedAsync 是定義自 IClaptrapMinionGrain 的方法,表示實時接收來自 MasterClaptrap 的事件通知。此處暫不展開說明,按照上文模板實現即可。
  2. WakeAsync 是定義自 IClaptrapMinionGrain 的方法,表示 MasterClaptrap 主動喚醒 Minion 的操作。此處暫不展開說明,按照上文模板實現即可。
  3. 當讀者檢視原始碼時,會發現該類被單獨定義在一個程式集當中。這只是一種分類辦法,可以理解為將 Minion 和 MasterClaptrap 分別放置在兩個不同的專案中進行分類。實際上放在一起也沒有問題。

註冊 Grain

此處,由於我們將 OrderDbGrain 定義在單獨的程式集,因此,需要額外的註冊這個程式集。如下所示:

  using System;
  using Autofac;
  using HelloClaptrap.Actors.Cart;
  using HelloClaptrap.Actors.DbGrains.Order;
  using HelloClaptrap.IActor;
  using HelloClaptrap.Repository;
  using Microsoft.AspNetCore.Hosting;
  using Microsoft.Extensions.Hosting;
  using Microsoft.Extensions.Logging;
  using Newbe.Claptrap;
  using Newbe.Claptrap.Bootstrapper;
  using NLog.Web;
  using Orleans;

  namespace HelloClaptrap.BackendServer
  {
      public class Program
      {
          public static void Main(string[] args)
          {
              var logger = NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger();
              try
              {
                  logger.Debug("init main");
                  CreateHostBuilder(args).Build().Run();
              }
              catch (Exception exception)
              {
                  //NLog: catch setup errors
                  logger.Error(exception, "Stopped program because of exception");
                  throw;
              }
              finally
              {
                  // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux)
                  NLog.LogManager.Shutdown();
              }
          }

          public static IHostBuilder CreateHostBuilder(string[] args) =>
              Host.CreateDefaultBuilder(args)
                  .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); })
                  .UseClaptrap(
                      builder =>
                      {
                          builder
                              .ScanClaptrapDesigns(new[]
                              {
                                  typeof(ICartGrain).Assembly,
                                  typeof(CartGrain).Assembly,
+                                 typeof(OrderDbGrain).Assembly
                              })
                              .ConfigureClaptrapDesign(x =>
                                  x.ClaptrapOptions.EventCenterOptions.EventCenterType = EventCenterType.OrleansClient);
                      },
                      builder => { builder.RegisterModule<RepositoryModule>(); })
                  .UseOrleansClaptrap()
                  .UseOrleans(builder => builder.UseDashboard(options => options.Port = 9000))
                  .ConfigureLogging(logging =>
                  {
                      logging.ClearProviders();
                      logging.SetMinimumLevel(LogLevel.Trace);
                  })
                  .UseNLog();
      }
  }

 

實現 EventHandler

+ using System.Threading.Tasks;
+ using HelloClaptrap.Models.Order.Events;
+ using HelloClaptrap.Repository;
+ using Newbe.Claptrap;
+ using Newtonsoft.Json;
+
+ namespace HelloClaptrap.Actors.DbGrains.Order.Events
+ {
+     public class OrderCreatedEventHandler
+         : NormalEventHandler<NoneStateData, OrderCreatedEvent>
+     {
+         private readonly IOrderRepository _orderRepository;
+
+         public OrderCreatedEventHandler(
+             IOrderRepository orderRepository)
+         {
+             _orderRepository = orderRepository;
+         }
+
+         public override async ValueTask HandleEvent(NoneStateData stateData,
+             OrderCreatedEvent eventData,
+             IEventContext eventContext)
+         {
+             var orderId = eventContext.State.Identity.Id;
+             await _orderRepository.SaveAsync(eventData.UserId, orderId, JsonConvert.SerializeObject(eventData.Skus));
+         }
+     }
+ }

 

  1. IOrderRepository 是直接操作儲存層的介面,用於訂單的增刪改查。此處呼叫該介面實現訂單資料庫的入庫操作。

註冊 EventHandler

實際上為了節約篇幅,我們已經在 “實現 Grain” 章節的程式碼中進行註冊。

實現 IInitialStateDataFactory

由於 StateData 沒有特殊定義,因此也不需要實現 IInitialStateDataFactory。

修改 Controller

樣例中,我們增加了 OrderController 用來下單和查詢訂單。讀者可以在原始碼進行檢視。

讀者可以使用一下步驟進行實際的效果測試:

  1. POST /api/cart/123 {“skuId”:”yueluo-666”,”count”:30} 向 123 號購物車加入 30 單位的 yueluo-666 號濃縮精華。
  2. POST /api/order {“userId”:”999”,”cartId”:”123”} 以 999 userId 的身份,從 123 號購物車進行下單。
  3. GET /api/order 下單成功後可以,通過該 API 檢視到下單完成的訂單。
  4. GET /api/sku/yueluo-666 可以通過 SKU API 檢視下單後的庫存餘量。

小結

至此,我們就完成了 “商品下單” 這個需求的基礎內容。通過該樣例可以初步瞭解多個 Claptrap 可以如何合作,以及如何使用 Minion 完成非同步任務。

不過,還有一些問題,我們將在後續展開討論。

您可以從以下地址來獲取本文章對應的原始碼:

最後但是最重要!

最近作者正在構建以反應式Actor模式事件溯源為理論基礎的一套服務端開發框架。希望為開發者提供能夠便於開發出 “分散式”、“可水平擴充套件”、“可測試性高” 的應用系統 ——Newbe.Claptrap

本篇文章是該框架的一篇技術選文,屬於技術構成的一部分。如果讀者對該內容感興趣,歡迎轉發、評論、收藏文章以及專案。您的支援是促進專案成功的關鍵。

聯絡方式:

您還可以查閱本系列的其他選文:

理論入門篇

  1. Newbe.Claptrap - 一套以 “事件溯源” 和 “Actor 模式” 作為基本理論的服務端開發框架

術語介紹篇

  1. Actor 模式
  2. 事件溯源(Event Sourcing)
  3. Claptrap
  4. Minion
  5. 事件 (Event)
  6. 狀態 (State)
  7. 狀態快照 (State Snapshot)
  8. Claptrap 設計圖 (Claptrap Design)
  9. Claptrap 工廠 (Claptrap Factory)
  10. Claptrap Identity
  11. Claptrap Box
  12. Claptrap 生命週期(Claptrap Lifetime Scope)
  13. 序列化(Serialization)

實現入門篇

  1. Newbe.Claptrap 框架入門,第一步 —— 建立專案,實現簡易購物車
  2. Newbe.Claptrap 框架入門,第二步 —— 簡單業務,清空購物車
  3. Newbe.Claptrap 框架入門,第三步 —— 定義 Claptrap,管理商品庫存

樣例實踐篇

  1. 構建一個簡易的火車票售票系統,Newbe.Claptrap 框架用例,第一步 —— 業務分析
  2. 線上體驗火車票售票系統

其他番外篇

  1. 談反應式程式設計在服務端中的應用,資料庫操作優化,從 20 秒到 0.5 秒
  2. 談反應式程式設計在服務端中的應用,資料庫操作優化,提速 Upsert
  3. 十萬同時線上使用者,需要多少記憶體?——Newbe.Claptrap 框架水平擴充套件實驗
  4. docker-mcr 助您全速下載 dotnet 映象
  5. 十多位全球技術專家,為你獻上近十個小時的.Net 微服務介紹
  6. 年輕的樵夫喲,你掉的是這個免費 8 核 4G 公網伺服器,還是這個隨時可用的 Docker 實驗平臺?

GitHub 專案地址:https://github.com/newbe36524/Newbe.Claptrap

Gitee 專案地址:https://gitee.com/yks/Newbe.Claptrap

您當前檢視的是先行釋出於 www.newbe.pro 上的部落格文章,實際開發文件隨版本而迭代。若要檢視最新的開發文件,需要移步 claptrap.newbe.pro

Newbe.Claptrap

 

相關文章