Bootstrap

Newbe.Claptrap 框架入门,第四步 —— 利用 Minion,商品下单

接上一篇  ,我们继续要了解一下如何使用 Newbe.Claptrap 框架开发业务。通过本篇阅读,您便可以开始学会在 Claptrap 框架中使用 Minion 进行异步的业务处理。

Newbe.Claptrap 是一个用于轻松应对并发问题的分布式开发框架。如果您是首次阅读本系列文章。建议可以先从本文末尾的入门文章开始了解。

开篇摘要

本篇,我通过实现 “商品下单” 的需求来了解一下如何在已有的项目样例中使用 Minion 来完成异步的业务处理。

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

本篇虽然重点在于 Minion 的使用,不过由于需要使用到一个新的 OrderGrain 对象,因此还是需要使用到前一篇 “定义 Claptrap” 的相关知识。

Minion 是一种特殊的 Claptrap,它与其 MasterClaptrap 之间的关系如下图所示:

其主体开发流程和 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 Skus { get; set; }
    }
}

OrderCreatedEvent

订单创建事件的定义如下:

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

namespace HelloClaptrap.Models.Order.Events
{
    public class OrderCreatedEvent : IEventData
    {
        public string UserId { get; set; }
        public Dictionary 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, 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(input.CartId);
            var items = await cartGrain.GetItemsAsync();

            // update inventory for each sku
            foreach (var (skuId, count) in items)
            {
                var skuGrain = _grainFactory.GetGrain(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);
        }
    }
}

通过 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
+     {
+     }
+ }

星际宗师:因为星际争霸比赛节奏快,信息量大,选手很容易忽视或误判部分信息,因此经常发生 “选手看不到发生在眼皮底下的关键事件” 的搞笑失误。玩家们由此调侃星际玩家都是瞎子(曾经真的有一场盲人和职业选手的对决),段位越高,瞎得越严重,职业星际选手清一色的盲人。

实现 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, IOrderDbGrain
+     {
+         public OrderDbGrain(IClaptrapGrainCommonService claptrapGrainCommonService)
+             : base(claptrapGrainCommonService)
+         {
+         }
+
+         public async Task MasterEventReceivedAsync(IEnumerable events)
+         {
+             foreach (var @event in events)
+             {
+                 await Claptrap.HandleEventAsync(@event);
+             }
+         }
+
+         public Task WakeAsync()
+         {
+             return Task.CompletedTask;
+         }
+     }
+ }

注册 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(); })
                  .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(); })
                  .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
+     {
+         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));
+         }
+     }
+ }

注册 EventHandler

实际上为了节约篇幅,我们已经在 “实现 Grain” 章节的代码中进行注册。

实现 IInitialStateDataFactory

由于 StateData 没有特殊定义,因此也不需要实现 IInitialStateDataFactory。

修改 Controller

样例中,我们增加了 OrderController 用来下单和查询订单。读者可以在源码进行查看。

读者可以使用一下步骤进行实际的效果测试:

小结

至此,我们就完成了 “商品下单” 这个需求的基础内容。通过该样例可以初步了解多个 Claptrap 可以如何合作,以及如何使用 Minion 完成异步任务。

不过,还有一些问题,我们将在后续展开讨论。

您可以从以下地址来获取本文章对应的源代码:

最后但是最重要!

最近作者正在构建以、和为理论基础的一套服务端开发框架。希望为开发者提供能够便于开发出 “分布式”、“可水平扩展”、“可测试性高” 的应用系统 ——Newbe.Claptrap

本篇文章是该框架的一篇技术选文,属于技术构成的一部分。如果读者对该内容感兴趣,欢迎转发、评论、收藏文章以及项目。您的支持是促进项目成功的关键。

联系方式:

  • 公开邮箱  (发送到该邮箱的内容将被公开)

您还可以查阅本系列的其他选文:

理论入门篇

术语介绍篇

实现入门篇

样例实践篇

其他番外篇

GitHub 项目地址:

Gitee 项目地址:

您当前查看的是先行发布于  上的博客文章,实际开发文档随版本而迭代。若要查看最新的开发文档,需要移步 

 

  • 本文作者: newbe36524

  • 本文链接: 

  • 版权声明: 本博客所有文章除特别声明外,均采用  许可协议。转载请注明出处!