.NET 云原生架构师训练营(基于 OP Storming 和 Actor 的大型分布式架构二)--学习笔记...

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

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

0933a2d08af07d17496e7fd29ead07f0.gif

学习分享 丨作者 / 郑 子 铭    

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

目录

  • 为什么我们用 Orleans

  • Dapr VS Orleans

  • Actor 模型

  • Orleans 的核心概念

  • 结合 OP Storming 的实践

结合 OP Storming 的实践

  • 业务模型

  • 设计模型

  • 代码实现

业务模型

28b37d56e30cdc8690f2aefee06feb8e.png

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

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

设计模型

d93d353ea16a9b621646fe0c38170e68.png

我们新建职位的时候需要一个参数对象 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 包

<PackageReference Include="Microsoft.Orleans.Core" Version="3.6.5" />
<PackageReference Include="Microsoft.Orleans.Server" Version="3.6.5" />
<PackageReference Include="Microsoft.Orleans.CodeGenerator.MSBuild" Version="3.6.5" />
<PackageReference Include="Microsoft.Orleans.OrleansTelemetryConsumers.Linux" Version="3.6.5" />
  • 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<IGrainLifecycle>
{
    public GrainReference GrainReference { get { return Data.GrainReference; } }
    
    /// <summary>
    /// String representation of grain's SiloIdentity including type and primary key.
    /// </summary>
    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<Job>, 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 包

<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />

在 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<Job>, 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<IActionResult> CreateAsync([FromBody] CreateJobViewModel model)
{
    var jobId = Guid.NewGuid().ToString();
    var jobGrain = _factory.GetGrain<IJobGrain>(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<Job> 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<Job>, IJobGrain
    {
        public async Task<Job> 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<IActionResult> CreateAsync([FromBody] CreateJobViewModel model)
{
    var jobId = Guid.NewGuid().ToString();
    var jobGrain = _factory.GetGrain<IJobGrain>(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<IActionResult> GetAsync(string jobId)
{
    var jobGrain = _factory.GetGrain<IJobGrain>(jobId);
}

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

using HelloOrleans.Host.Contract.Entity;
using Orleans;

namespace HelloOrleans.Host.Contract.Grain
{
    public interface IJobGrain : IGrainWithStringKey
    {
        Task Create(Job job);
        Task<Job> 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<Job>, IJobGrain
    {
        public async Task Create(Job job)
        {
            this.State = job;
            await this.WriteStateAsync();
        }

        public Task<Job> 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<IActionResult> GetAsync(string jobId)
{
    var jobGrain = _factory.GetGrain<IJobGrain>(jobId);
    return Ok(await jobGrain.Get());
}

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

[Route("{jobId}")]
[HttpGet]
public async Task<IActionResult> GetAsync(string jobId)
{
    if (string.IsNullOrEmpty(jobId))
    {
        throw new ArgumentNullException(nameof(jobId));
    }

    var jobGrain = _factory.GetGrain<IJobGrain>(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": "第一个职位"
}

7e8b647eb508fb6385d3f8088c6a1a6b.png

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

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

b99c2a078f6dec50b562570b350f403a.png

可以看到方法调用成功,返回同一个 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 交流学习群


长按识别下方二维码,或点击阅读原文。和我一起,交流学习,分享心得。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,以下是一个简单的cmakelists.txt文件示例,其中涵盖了一些基本的语法和命令: ``` cmake_minimum_required(VERSION 3.10) # 指定CMake最低版本要求 project(MyProject VERSION 1.0) # 指定项目名称和版本号 set(CMAKE_CXX_STANDARD 11) # 指定C++标准版本 add_executable(myapp main.cpp) # 添加可执行文件并指定源代码文件 find_package(Boost REQUIRED) # 查找Boost库并指定为依赖项 target_link_libraries(myapp PRIVATE Boost::boost) # 将Boost库链接到应用程序 ``` 上述cmakelists.txt文件中,每个命令都有其特定的作用: - `cmake_minimum_required(VERSION 3.10)`:指定CMake最低版本要求,这里是3.10版本。 - `project(MyProject VERSION 1.0)`:指定项目名称和版本号,这里是MyProject和1.0。 - `set(CMAKE_CXX_STANDARD 11)`:指定C++标准版本,这里是C++11。 - `add_executable(myapp main.cpp)`:添加可执行文件并指定源代码文件,这里是将main.cpp编译成名为myapp的可执行文件。 - `find_package(Boost REQUIRED)`:查找Boost库并指定为依赖项,这里查找名称为Boost的库。 - `target_link_libraries(myapp PRIVATE Boost::boost)`:将Boost库链接到应用程序,并将链接方式指定为私有的,这里将Boost库链接到myapp可执行文件。 通过cmakelists.txt文件,我们可以告诉CMake如何构建我们的项目,包括指定编译器、编译选项、依赖项等。在生成Makefile文件后,我们可以通过运行make命令来编译和构建项目。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值