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


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

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

開篇摘要

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

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

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

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

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

Minion

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

步驟 Claptrap Minion
定義 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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM