Orleans 的优势之一就是:支持有状态服务的水平扩展。那这一节我们就来看看如何来了解下有状态的Grain。
先来看下上节中定义的Grain:SessionControlGrain
public class SessionControlGrain : Grain, ISessionControlGrain
{
private List<string> LoginUsers { get; set; } = new List<string>();
public Task Login(string userId)
{
//获取当前Grain的身份标识(因为ISessionControlGrain身份标识为string类型,GetPrimaryKeyString());
var appName = this.GetPrimaryKeyString();
LoginUsers.Add(userId);
Console.WriteLine($"Current active users count of {appName} is {LoginUsers.Count}");
return Task.CompletedTask;
}
public Task Logout(string userId)
{
//获取当前Grain的身份标识
var appName = this.GetPrimaryKey();
LoginUsers.Remove(userId);
Console.WriteLine($"Current active users count of {appName} is {LoginUsers.Count}");
return Task.CompletedTask;
}
public Task<int> GetActiveUserCount()
{
return Task.FromResult(LoginUsers.Count);
}
}
上面的Grain中定义属性private List<string> LoginUsers { get; set; } = new List<string>();
用来保存登录状态,其是保存在内存中的,一旦服务奔溃或重启,维护的状态数据就会丢失。
很显然,这在真实应用场景中不被允许。
在第一节中,已经对有状态和无状态有了解释,关键的区别在于:状态数据的是否持久化。因此上面针对ISessionControlGrain
的实现SessionControlGrain
是无状态的。
那接下来就来看看如何用有状态的Grain来实现!
针对统计登录用户的需求来说,其中的状态数据就是在线用户列表,所以可以直接定义一个LoginState
来将行为和数据解耦。
/// <summary>
/// 登录状态
/// </summary>
public class LoginState
{
public List<string> LoginUsers { get; set; } = new List<string>();
public int Count => LoginUsers.Count;
}
紧接着就可以重新实现一个ISessionControlGrain
,如下:
/// <summary>
/// 有状态的Grain
/// </summary>
public class SessionControlStateGrain : Grain<LoginState>, ISessionControlGrain
{
public Task Login(string userId)
{
var appName = this.GetPrimaryKeyString();
this.State.LoginUsers.Add(userId);
this.WriteStateAsync();
Console.WriteLine($"Current active users count of {appName} is {this.State.Count}");
return Task.CompletedTask;
}
public Task Logout(string userId)
{
//获取当前Grain的身份标识
var appName = this.GetPrimaryKey();
this.State.LoginUsers.Remove(userId);
this.WriteStateAsync();
Console.WriteLine($"Current active users count of {appName} is {this.State.Count}");
return Task.CompletedTask;
}
public Task<int> GetActiveUserCount()
{
return Task.FromResult(this.State.Count);
}
}
对比两个Grain的实现,有状态的Grain主要有以下变化:
Grain<T>
,其中T
用来指定当前Grain的附属状态对象。this.State
来操作状态this.WriteStateAsync();
来显式持久化状态。那Grain的状态保存到哪里去了呢?
开发环境下,可使用内存作为Grain的状态仓库。仅需在构建Orleans Silo时配置AddMemoryGrainStorageAsDefault()
即可,如下所示:
return Host.CreateDefaultBuilder()
.UseOrleans((builder) =>
{
builder.UseLocalhostClustering()
.AddMemoryGrainStorageAsDefault()
.Configure<ClusterOptions>(options =>
{
options.ClusterId = "Hello.Orleans";
options.ServiceId = "Hello.Orleans";
})
.Configure<EndpointOptions>(options => options.AdvertisedIPAddress = IPAddress.Loopback)
.ConfigureApplicationParts(parts =>
parts.AddApplicationPart(typeof(ISessionControlGrain).Assembly).WithReferences());
}
)
存在内存中,只是为了方便开发,显然在生产环境中是万万不可的。因此,可选择其他存储介质进行持久化。比如数据库等,Orleans 官方维护的状态持久化提供者有以下几种:
当然除此之外,社区也维护系列开源项目支持将状态数据持久化到其他介质。 接下来就来讲解如何持久化状态数据到SQL Server 数据库。
SqlServer的配置并没有想象的那样简单,根据官方文档: Configuring ADO.NET Providers、 ADO.NET Database Configuration,你会发现需要执行以下几步:
Microsoft.Orleans.Persistence.AdoNet
NuGet包的引用System.Data.SqlClient
NuGet包的引用为了简化配置,我做了一个简单的包装项目Orleans.AdoNet.Extensions,以简化SqlServer、MySql、Oracle和PostgreSql 的配置。以Sql Server 为例,仅需:
Orleans.AdoNet.SqlServer
包Host.CreateDefaultBuilder()
.UseOrleans((builder) =>
{
var connectionString =
@"Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=Hello.Orleans;Integrated Security=True;Pooling=False;Max Pool Size=200;MultipleActiveResultSets=True";
//use AdoNet for Persistence
builder.AddSqlServerGrainStorageAsDefault(options =>
{
options.ConnectionString = connectionString;
options.UseJsonFormat = true;
});
重新运行项目,查询数据库,你会发现状态数据,实际上是持久化到Storage
表中了。如下图所示: