1. 程式人生 > 其它 >Dapr牽手.NET學習筆記:Actor小試

Dapr牽手.NET學習筆記:Actor小試

Actor模型是一種避免執行緒共享資料,相同Actor實體序列化的方案,所以不便dapr的其他功能,幾乎都是非程式設計入侵的,相反,Dapr Acror深度定製的,關於Actor,.net中有一些通用框架,比如Akka.net,微軟的Orleans,還有最近復活的Proto actor。Dapr下的Actor,是dapr實現了一些庫,基於這些庫來實現actor模型程式設計的。

本篇開個小頭,實際體會一下actor的作用,actor的一大作用就是例項隔離,相同例項不共享記憶體,不同例項間還是可以並行的,當然這個實現並不與OOP中的例項相等,還是看下面這個小例子吧,通過程式碼來感覺。

一、首先定義一個類庫專案

需要引用Nuget包 Dapr.Actors

public interface IAccountActor : IActor
{
    Task<string> GetTimeAsync(string inTime);
}

二、定義一個asp.net api專案

實現上面定義的介面,需要引入Nuget包Dapr.Actors.AspNetCore

public class AccountActor: Actor, IAccountActor
 {      
     public AccountActor(ActorHost host) : base(host)
     {           
     }
     
public async Task<string> GetTimeAsync(string inTime) { Console.WriteLine($"{this.Id}開始"); Task.Delay(3000).Wait(); Console.WriteLine($"{this.Id}結束"); return await Task.FromResult($"Actor ID:{this.Id} 傳入時間:{inTime},返回時間:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"
)}"); } }

需要在向Services中注入Actor

using OrderFactoryService;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
//注入Actor
builder.Services.AddActors(options =>
{
    options.HttpEndpoint = "http://localhost:3999";    
    options.Actors.RegisterActor<AccountActor>();
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
app.UseAuthorization();

app.UseRouting();
app.UseEndpoints(endpoints =>
{   
   //Map Actor Handler
    endpoints.MapActorsHandlers();
});
app.MapControllers();
app.Run();

為了對比測試,可以定義一個/gettime的api,比較並序列

[ApiController]
[Route("[controller]")]
public class HomeController : ControllerBase
{
    private readonly ILogger<HomeController> _logger;

    public HomeController(ILogger<HomeController> logger)
    {
        _logger = logger;
    }

    [HttpGet("/gettime")]
    public IActionResult Get(string inTime)
    {
        Task.Delay(3000).Wait();
        return Ok($"傳入時間:{inTime},返回時間:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
    }
}

三、新增一個Actor客戶端專案

需要引用Nuget包 Dapr.Actors

using Dapr.Actors;
using Dapr.Actors.Client;
using IOrderFactoryActory.Interfaces;

Console.WriteLine("回車開始");
Console.ReadLine();

//呼叫api是並行的
var client = new HttpClient();
var httpTask1 = new Task(async () =>
{
    Console.WriteLine(await client.GetStringAsync("http://localhost:5000/gettime?intime=" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")));
});
var httpTask2 = new Task(async () =>
{
    Console.WriteLine(await client.GetStringAsync("http://localhost:5000/gettime?intime=" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")));
});
httpTask1.Start();
httpTask2.Start();

//相同ID的actor是序列的,不同ID的actor是並行的
var factory = new ActorProxyFactory(new ActorProxyOptions { HttpEndpoint = "http://localhost:3999" });
var account1 = CreateActor(factory, "11111111111");
var account2 = CreateActor(factory, "22222222222");
var actorTask1_1 = new Task(async () =>
{
    Console.WriteLine(await account1.GetTimeAsync(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")));
});
var actorTask1_2 = new Task(async () =>
{
    Console.WriteLine(await account1.GetTimeAsync(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")));
});
var actorTask2 = new Task(async () =>
{
    Console.WriteLine(await account2.GetTimeAsync(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")));
});
actorTask1_1.Start();
actorTask1_2.Start();
actorTask2.Start();

Console.WriteLine("回車結束");
Console.ReadLine();

static IAccountActor CreateActor(ActorProxyFactory factory, string accountNo)
{
    var actorType = "AccountActor";
    var actorId = new ActorId(accountNo);   
    return factory.CreateActorProxy<IAccountActor>(actorId, actorType);
}

四、開始測試

啟動sidecar

dapr run --app-id account --app-port 5000 --dapr-http-port 3999

執行結果:

  可以通過上面的例子看到,web api的傳入時間和返回時間幾乎相同,說明他們是並行執行的,都在內部等了3秒;Actor有兩個例項,是通過ActorID來區分例項的,ID為1開頭的兩個例項雖然傳入時間幾乎相同,但在返回時間上,第二次明顯是排在第一次返回後的(這正是Actor的序列基本準則),ID為2開頭的,可以與1並行。

  實際場景是什麼呢?前一段時間開發了一套賬務系統,場景是有很多賬戶批量入帳,當然有可能有相同帳戶同時入帳,入帳時需要取出舊的帳戶餘額,加上本次入帳金額,然後更新掉帳戶餘額;因為是通過web api調過來的併發,處理辦法是在表的資料行上用行級鎖(DBA會罵孃的),保證兩個相同帳戶入帳時,不會同時取,然後都用舊餘額相加。但如果這裡用Actor,就可以釋放資料庫的壓力(DBA會很開心的),相同帳戶的Actor是序列執行,所以在業務層就避免了併發,不同帳戶不受影響,關鍵是Actor是細小的顆粒,可以大量建立銷燬。篇幅和時間所限,下一篇會用例子來實現這個場景。

 想要更快更方便的瞭解相關知識,可以關注微信公眾號