首页 > 今日焦点  >  正文
世界百事通!.NET 云原生架构师训练营(基于 OP Storming 和 Actor 的大型分布式架构二)--学习笔记
2022-12-31 23:46:13 来源:

▲点击上方“DotNet NB”关注公众号

回复“1”获取开发者路线图


(相关资料图)

学习分享丨作者/郑子铭

这是DotNetNB公众号的第202篇原创文章

目录

为什么我们用 Orleans

Dapr VS Orleans

Actor 模型

Orleans 的核心概念

结合 OP Storming 的实践

结合 OP Storming 的实践

业务模型

设计模型

代码实现

业务模型

我们可以把关键对象(职位、客户行为记录、线索)参考为 actor

猎头顾问一边寻找职位,一边寻找候选人,撮合之后匹配成线索,然后推荐候选人到客户公司,进行面试,发放 offer,候选人入职

设计模型

我们新建职位的时候需要一个参数对象 CreateJobArgument,相当于录入数据

创建了 Job 之后,它有三个行为:浏览、点赞、投递

投递之后会直接产生一个意向的 Thread,可以继续去推进它的状态:推荐 -> 面试 -> offer -> 入职

针对浏览和点赞会产生两种不同的活动记录:ViewActivity 和 StarActivity

代码实现

HelloOrleans.Host

HelloOrleans.Host

新建一个空白解决方案 HelloOrleans

创建一个 ASP .NET Core 空项目 HelloOrleans.Host

分别创建 BaseEntity、Job、Thread、Activity 实体

namespace HelloOrleans.Host.Contract.Entity{    public class BaseEntity    {        public string Identity { get; set; }    }}namespace HelloOrleans.Host.Contract.Entity{    public class Job : BaseEntity    {        public string Title { get; set; }        public string Description { get; set; }        public string Location { get; set; }    }}namespace HelloOrleans.Host.Contract.Entity{    public class Thread : BaseEntity    {        public string JobId { get; set; }        public string ContactId { get; set; }        public EnumThreadStatus Status { get; set; }    }}namespace HelloOrleans.Host.Contract{    public enum EnumThreadStatus : int    {        Recommend,        Interview,        Offer,        Onboard,    }}namespace HelloOrleans.Host.Contract.Entity{    public class Activity : BaseEntity    {        public string JobId { get; set; }        public string ContactId { get; set; }        public EnumActivityType Type { get; set; }    }}namespace HelloOrleans.Host.Contract{    public enum EnumActivityType : int    {        View = 1,        Star = 2,    }}

给 Job 添加 View 和 Star 的行为

public async Task View(string contactId){}public async Task Star(string contactId){}

这里就只差 Grain 的 identity,我们添加 Orleans 的 nuget 包

Microsoft.Orleans.Core 是核心

Microsoft.Orleans.Server 做 Host 就需要用到它

Microsoft.Orleans.CodeGenerator.MSBuild 会在编译的时候帮我们生成客户端或者访问代码

Microsoft.Orleans.OrleansTelemetryConsumers.Linux 是监控

安装完后我们就可以继承 Grain 的基类了

using Orleans;namespace HelloOrleans.Host.Contract.Entity{    public class Job : Grain    {        public string Title { get; set; }        public string Description { get; set; }        public string Location { get; set; }        public async Task View(string contactId)        {        }        public async Task Star(string contactId)        {        }    }}

如果我们需要用它来做持久化是有问题的,因为持久化的时候它会序列化我们所有的公有属性,然而在 Grain 里面会有一些公有属性你没有办法给它序列化,所以持久化的时候会遇到一些问题,除非我们把持久化的东西重新写一遍

public abstract class Grain : IAddressable, ILifecycleParticipant{    public GrainReference GrainReference { get { return Data.GrainReference; } }        ///     /// String representation of grain"s SiloIdentity including type and primary key.    ///     public string IdentityString    {        get { return Identity?.IdentityString ?? string.Empty; }    }        ...}

理论上你的状态和行为是可以封装在一起的,这样更符合 OO 的逻辑

我们现在需要分开状态和行为

定义一个 IJobGrain 接口,继承 IGrainWithStringKey,用 string 作为它的 identity 的类型

using Orleans;namespace HelloOrleans.Host.Contract.Grain{    public interface IJobGrain : IGrainWithStringKey    {        Task View(string contactId);    }}

定义 JobGrain 继承 Grain,实现 IJobGrain 接口

using HelloOrleans.Host.Contract.Entity;using HelloOrleans.Host.Contract.Grain;using Orleans;namespace HelloOrleans.Host.Grain{    public class JobGrain : Grain, IJobGrain    {        public Task View(string contactId)        {            throw new NotImplementedException();        }    }}

这是使用 DDD 来做的区分开状态和行为,变成贫血模型,是不得已而为之,因为持久化的问题

在 Orleans 的角度而言,它的 Actor 绑定了一个外部的状态,但是实际上我们更希望它们两在一起

它的实体就变成这样

namespace HelloOrleans.Host.Contract.Entity{    public class Job    {        public string Title { get; set; }        public string Description { get; set; }        public string Location { get; set; }    }}

Job 不是 Actor 实例,JobGrain 才是 Actor 实例

接下来我们需要做一个 Host 让它跑起来

添加 nuget 包

在 Program 中需要通过 WebApplication 的 Builder 配置 Orleans

builder.Host.UseOrleans(silo =>{    silo.UseLocalhostClustering();    silo.AddMemoryGrainStorage("hello-orleans");});

在 JobGrain 中使用 hello-orleans 这个 Storage 标识一下

[StorageProvider(ProviderName = "hello-orleans")]public class JobGrain : Grain, IJobGrain

添加 JobController,这属于前面讲的 silo 内模式,可以直接使用 IGrainFactory,因为这是在同一个项目里

using Microsoft.AspNetCore.Mvc;using Orleans;namespace HelloOrleans.Host.Controllers{    [Route("job")]    public class JobController : Controller    {        private IGrainFactory _factory;        public JobController(IGrainFactory grainFactory)        {            _factory = grainFactory;        }    }}

添加一个创建方法 CreateAsync,它的入参叫做 CreateJobViewModel,包含我们需要的 Job 的数据

[Route("")][HttpPost]public async Task CreateAsync([FromBody] CreateJobViewModel model){    var jobId = Guid.NewGuid().ToString();    var jobGrain = _factory.GetGrain(jobId);}

创建的时候 Grain 是不存在的,必须有 identity,不然 Actor 获取不到,所以需要先 new 一个 identity,就是 jobId

通过 IGrainFactory 获取到 jobGrain 之后我们是无法获取到它的 state,只能看到它的行为,所以我们需要在 Grain 里面添加一个 Create 的方法方便我们调用

using HelloOrleans.Host.Contract.Entity;using Orleans;namespace HelloOrleans.Host.Contract.Grain{    public interface IJobGrain : IGrainWithStringKey    {        Task Create(Job job);        Task View(string contactId);    }}

所以这个 Create 方法并不是真正的 Create,只是用来设置 state 的对象,再通过 WriteStateAsync 方法保存

using HelloOrleans.Host.Contract.Entity;using HelloOrleans.Host.Contract.Grain;using Orleans;using Orleans.Providers;namespace HelloOrleans.Host.Grain{    [StorageProvider(ProviderName = "hello-orleans")]    public class JobGrain : Grain, IJobGrain    {        public async Task Create(Job job)        {            job.Identity = this.GetPrimaryKeyString();            this.State = job;            await this.WriteStateAsync();            return this.State;        }        public Task View(string contactId)        {            throw new NotImplementedException();        }    }}

new 一个 job,调用 Create 方法设置 State,得到一个带 identity 的 job,然后返回 OK

[Route("")][HttpPost]public async Task CreateAsync([FromBody] CreateJobViewModel model){    var jobId = Guid.NewGuid().ToString();    var jobGrain = _factory.GetGrain(jobId);    var job = new Job()    {        Title = model.Title,        Description = model.Description,        Location = model.Location,    };    job = await jobGrain.Create(job);    return Ok(job);}

因为我们现在采用的是内存级别的 GrainStorage,所以我们没有办法去查看它

我们再加一个 Get 的方法去查询它

[Route("{jobId}")][HttpGet]public async Task GetAsync(string jobId){    var jobGrain = _factory.GetGrain(jobId);}

这个时候我们需要去 Grain 的接口里面加一个 Get 方法

using HelloOrleans.Host.Contract.Entity;using Orleans;namespace HelloOrleans.Host.Contract.Grain{    public interface IJobGrain : IGrainWithStringKey    {        Task Create(Job job);        Task Get();        Task View(string contactId);    }}

Get 方法是不需要传 id 的,因为这个 id 就是 Grain 的 id,你激活的时候就已经有了,直接返回 this.State

using HelloOrleans.Host.Contract.Entity;using HelloOrleans.Host.Contract.Grain;using Orleans;using Orleans.Providers;namespace HelloOrleans.Host.Grain{    [StorageProvider(ProviderName = "hello-orleans")]    public class JobGrain : Grain, IJobGrain    {        public async Task Create(Job job)        {            this.State = job;            await this.WriteStateAsync();        }        public Task Get()        {            return Task.FromResult(this.State);        }        public Task View(string contactId)        {            throw new NotImplementedException();        }    }}

这个地方所有你的行为都不是直接去查数据库,而是利用这个 State,它不需要你自己去读取,跟 DDD 的 repository 不同

直接通过 Grain 的 Get 方法获取 Job 返回 OK

[Route("{jobId}")][HttpGet]public async Task GetAsync(string jobId){    var jobGrain = _factory.GetGrain(jobId);    return Ok(await jobGrain.Get());}

这里我们可以再加点校验逻辑

[Route("{jobId}")][HttpGet]public async Task GetAsync(string jobId){    if (string.IsNullOrEmpty(jobId))    {        throw new ArgumentNullException(nameof(jobId));    }    var jobGrain = _factory.GetGrain(jobId);    return Ok(await jobGrain.Get());}

要注意如果你传入的 jobId 是不存在的,因为不管你传什么,只要是一个合法的字符串,并且不重复,它都会帮你去激活,只不过在于它是否做持久化而已,如果你随便传了一个 jobId,这个时候不是调了 Get 方法,它可能也会返回给你一个空的 state,所以这个 jobId 没有这种很强的合法性的约束,在调 Get 的时候要特别的注意,不管是 Create 还是 Get,其实都是调用了 GetGrain,传了一个 identity 进去,这样的一个行为

在 Program 中添加 Controller 的配置

using Orleans.Hosting;var builder = WebApplication.CreateBuilder(args);builder.Host.UseOrleans(silo =>{    silo.UseLocalhostClustering();    silo.AddMemoryGrainStorage("hello-orleans");});builder.Services.AddControllers();var app = builder.Build();app.UseRouting();app.UseEndpoints(endpoints =>{    endpoints.MapControllers();});app.MapGet("/", () => "Hello World!");app.Run();

我们启动项目测试一下

Create 方法入参

{"title": "第一个职位","description": "第一个职位"}

可以看到方法调用成功,返回的 job 里面包含了 identity

接着我们使用 Create 方法返回的 identity 作为入参调用 Get 方法

可以看到方法调用成功,返回同一个 job

这种基于内存的存储就很适合用来做单元测试

推荐阅读:.NET周报【12月第1期 2022-12-08】.NET 7 新增的 IParsable 接口介绍.NET 云原生架构师训练营(基于 OP Storming 和 Actor 的大型分布式架构一)--学习笔记一个.NetCore前后端分离、模块化、插件式的通用框架.NET 为什么推荐Kestrel作为网络开发框架用最少的代码打造一个Mini版的gRPC框架

点击下方卡片关注DotNet NB

一起交流学习

▲点击上方卡片关注DotNet NB,一起交流学习

请在公众号后台

回复【路线图】获取.NET 2021开发者路线图回复【原创内容】获取公众号原创内容回复【峰会视频】获取.NET Conf开发者大会视频回复【个人简介】获取作者个人简介回复【年终总结】获取作者年终总结回复【加群】加入DotNet NB交流学习群长按识别下方二维码,或点击阅读原文。和我一起,交流学习,分享心得。

标签: 我们需要 这个时候 年终总结

精彩放送