粒度持久性

谷粒可以有多个关联的命名的持久数据对象。 这些状态对象在粒度激活期间从存储加载,以便在请求期间可用。 粒度持久性使用可扩展的插件模型,允许你为任何数据库使用存储提供程序。 此持久性模型旨在简单起见,并不旨在涵盖所有数据访问模式。 粒度还可以直接访问数据库,而无需使用粒度持久性模型。

粒度持久性图

在上图中,UserGrain 具有 配置文件 状态和 Cart 状态,每个状态都存储在单独的存储系统中。

目标

  1. 支持每个粒度的多个命名持久性数据对象。
  2. 允许多个配置的存储提供程序,每个提供程序可能具有不同的配置,并由不同的存储系统提供支持。
  3. 使社区能够开发和发布存储提供程序。
  4. 为存储提供程序提供对在持久后备存储中存储粒度状态数据的方式的完全控制。 推论:Orleans 不提供全面的 ORM 存储解决方案,但允许自定义存储提供程序根据需要支持特定的 ORM 要求。

包裹

可以在 Orleans 上找到粮食存储提供程序。 正式维护的包包括:

API(应用程序编程接口)

谷物通过 IPersistentState<TState> 与其持久状态进行交互,其中 TState 是可序列化的状态类型。

public interface IPersistentState<TState> : IStorage<TState>
{
}

public interface IStorage<TState> : IStorage
{
    TState State { get; set; }
}

public interface IStorage
{
    string Etag { get; }

    bool RecordExists { get; }

    Task ClearStateAsync();

    Task WriteStateAsync();

    Task ReadStateAsync();
}
public interface IPersistentState<TState> where TState : new()
{
    TState State { get; set; }

    string Etag { get; }

    Task ClearStateAsync();

    Task WriteStateAsync();

    Task ReadStateAsync();
}

Orleans 将 IPersistentState<TState> 的实例作为构造函数参数注入到粒中。 可以使用属性 PersistentStateAttribute 注释这些参数,以标识要注入的状态的名称以及提供状态的存储提供程序的名称。 以下示例通过向构造函数注入两个命名状态来演示这一点 UserGrain

public class UserGrain : Grain, IUserGrain
{
    private readonly IPersistentState<ProfileState> _profile;
    private readonly IPersistentState<CartState> _cart;

    public UserGrain(
        [PersistentState("profile", "profileStore")] IPersistentState<ProfileState> profile,
        [PersistentState("cart", "cartStore")] IPersistentState<CartState> cart)
    {
        _profile = profile;
        _cart = cart;
    }
}

不同的粒度类型可以使用不同的配置的存储提供程序,即使两者都是相同的类型(例如,连接到不同 Azure 存储帐户的两个不同的 Azure 表存储提供程序实例)。

读取状态

粒子的状态在粒子激活时自动读取,但粒子负责在必要时显式触发任何已更改的粒子状态的写入。

如果某个 grain 希望从后备存储中显式重新读取其最新状态,则应调用方法 ReadStateAsync。 这会通过存储提供程序从永久性存储重新加载粒度状态。 当Task中的ReadStateAsync()完成时,将覆盖并替换先前内存中的粒度状态副本。

使用 State 属性访问状态的值。 例如,以下方法访问上述代码中声明的配置文件状态:

public Task<string> GetNameAsync() => Task.FromResult(_profile.State.Name);

无需在正常作期间调用 ReadStateAsync() ; Orleans 在激活期间自动加载状态。 但是,可以使用 ReadStateAsync() 来刷新被外部修改的状态。

有关错误处理机制的详细信息,请参阅下面的 “故障模式 ”部分。

写入状态

您可以通过State属性来修改状态。 修改的状态不会自动持久化。 而是通过调用 WriteStateAsync 方法决定何时保留状态。 例如,以下方法更新 State 上的属性并保留更新的状态。

public async Task SetNameAsync(string name)
{
    _profile.State.Name = name;
    await _profile.WriteStateAsync();
}

从概念上讲, Orleans 运行时会获取粒度状态数据对象的深层副本,以便在任何写入作期间使用。 在幕后,运行时 可能会 使用优化规则和启发式,以避免在某些情况下执行部分或全部深层复制,前提是保留预期的逻辑隔离语义。

有关错误处理机制的详细信息,请参阅下面的 “失败模式 ”部分。

清除状态

该方法 ClearStateAsync 清除存储中的粒度状态。 根据提供程序,此作可能会选择完全删除粒度状态。

开始吧

在粒度可以使用持久性之前,必须在接收器上配置存储提供程序。

首先,配置存储提供程序:一个用于用户资料状态,一个用于购物车状态。

using IHost host = new HostBuilder()
    .UseOrleans(siloBuilder =>
    {
        siloBuilder.AddAzureTableGrainStorage(
            name: "profileStore",
            configureOptions: options =>
            {
                // Configure the storage connection key
                options.ConfigureTableServiceClient(
                    "DefaultEndpointsProtocol=https;AccountName=data1;AccountKey=SOMETHING1");
            })
            .AddAzureBlobGrainStorage(
                name: "cartStore",
                configureOptions: options =>
                {
                    // Configure the storage connection key
                    options.ConfigureTableServiceClient(
                        "DefaultEndpointsProtocol=https;AccountName=data2;AccountKey=SOMETHING2");
                });
    })
    .Build();
var host = new HostBuilder()
    .UseOrleans(siloBuilder =>
    {
        siloBuilder.AddAzureTableGrainStorage(
            name: "profileStore",
            configureOptions: options =>
            {
                // Use JSON for serializing the state in storage
                options.UseJson = true;

                // Configure the storage connection key
                options.ConnectionString =
                    "DefaultEndpointsProtocol=https;AccountName=data1;AccountKey=SOMETHING1";
            })
            .AddAzureBlobGrainStorage(
                name: "cartStore",
                configureOptions: options =>
                {
                    // Use JSON for serializing the state in storage
                    options.UseJson = true;

                    // Configure the storage connection key
                    options.ConnectionString =
                        "DefaultEndpointsProtocol=https;AccountName=data2;AccountKey=SOMETHING2";
                });
    })
    .Build();

重要

Microsoft 建议使用最安全的可用身份验证流。 如果要连接到 Azure SQL,建议使用 Azure 资源的托管标识这种身份验证方法。

配置了名为"profileStore"的存储提供程序后,可以从Grain访问此提供程序。

可以通过两种主要方式来将持久状态添加到Grain对象中:

  1. 通过将IPersistentState<TState>注入到grain对象的构造函数中。
  2. 继承自 Grain<TGrainState>.

向 grain 添加存储的推荐方法是将 IPersistentState<TState> 注入到具有相关 [PersistentState("stateName", "providerName")] 属性的 grain 构造函数中。 有关 Grain<TState> 的详细信息,请参阅 下文使用 Grain<TState> 将存储添加到粮食。 仍支持使用 Grain<TState> ,但被视为旧方法。

声明一个类以保存粒度的状态:

[Serializable]
public class ProfileState
{
    public string Name { get; set; }

    public Date DateOfBirth { get; set; }
}

IPersistentState<ProfileState> 注入到粒的构造函数中。

public class UserGrain : Grain, IUserGrain
{
    private readonly IPersistentState<ProfileState> _profile;

    public UserGrain(
        [PersistentState("profile", "profileStore")]
        IPersistentState<ProfileState> profile)
    {
        _profile = profile;
    }
}

重要

配置状态在注入到构造函数时不会加载,因此那时访问它是无效的。 在调用 OnActivateAsync 之前,将加载状态。

现在,粒度具有持久性状态,可以添加用于读取和写入状态的方法:

public class UserGrain : Grain, IUserGrain
{
    private readonly IPersistentState<ProfileState> _profile;

    public UserGrain(
        [PersistentState("profile", "profileStore")]
        IPersistentState<ProfileState> profile)
    {
        _profile = profile;
    }

    public Task<string> GetNameAsync() => Task.FromResult(_profile.State.Name);

    public async Task SetNameAsync(string name)
    {
        _profile.State.Name = name;
        await _profile.WriteStateAsync();
    }
}

持久性操作的失败模式

读取操作的失败模式

存储提供程序在初始读取特定粒子的状态数据时返回的错误会导致该粒子的激活操作失败。 在这种情况下,不会调用该粒度的 OnActivateAsync 生命周期回调方法。 导致激活故障的粒度的原始请求会返回到调用方,就像粒度激活期间发生的任何其他故障一样。 读取特定粒度的状态数据时,存储提供程序遇到的故障会在程序中引发异常。ReadStateAsyncTask 粒度可以选择处理或忽略Task异常,就像在Task中的任何其他Orleans异常一样。

任何尝试向由于缺少或错误的存储提供程序配置导致在接收器启动时未能加载的粒发送消息的操作,都会返回永久错误 BadProviderConfigException

写入操作的失败模式

为特定grain写入状态数据时,存储提供程序遇到的故障会导致WriteStateAsync()Task抛出异常。 通常,这意味着将粒度调用异常抛回到客户端调用方,前提是 WriteStateAsync()Task 已正确链接到此粒度方法的最终返回 Task 。 但是,在某些高级场景中,可以编写特定的代码来专门应对此类写入错误,就像处理其他任何已发生错误一样Task

执行错误处理或恢复代码的模块必须捕获异常或错误状态,而不是重新引发异常,这表示它们已成功处理写入错误。

建议

使用 JSON 序列化或其他版本容错序列化格式

代码不断发展,这通常包括存储类型。 若要适应这些更改,请配置适当的序列化程序。 对于大多数存储提供程序,可以使用一个选项 UseJson 或类似选项将 JSON 用作序列化格式。 请确保在不断发展的数据协定时,仍可加载已存储的数据。

使用 Grain<TState> 为谷粒增加存储

重要

使用 Grain<T> 向一个粒增加存储被视为 遗留 功能。 使用IPersistentState<T>如前所述添加粮食储存。

继承自 Grain<T> (其中 T 需要持久性的应用程序特定状态数据类型)的粒度类会自动从指定的存储加载其状态。

使用指定存储提供程序的命名实例标记此类粒度 StorageProviderAttribute ,以便读取/写入此粒度的状态数据。

[StorageProvider(ProviderName="store1")]
public class MyGrain : Grain<MyGrainState>, /*...*/
{
  /*...*/
}

Grain<T> 类定义要调用的子类的以下方法:

protected virtual Task ReadStateAsync() { /*...*/ }
protected virtual Task WriteStateAsync() { /*...*/ }
protected virtual Task ClearStateAsync() { /*...*/ }

这些方法的行为与前面定义的对应项相对应 IPersistentState<TState>

创建存储提供程序

zh-CN: 状态持久性 API 包含两个部分:一是通过 IPersistentState<T>Grain<T> 暴露给 Grain 的 API,二是以 IGrainStorage 为中心的存储提供程序 API,这个接口是存储提供程序必须实现的。

/// <summary>
/// Interface to be implemented for a storage able to read and write Orleans grain state data.
/// </summary>
public interface IGrainStorage
{
    /// <summary>Read data function for this storage instance.</summary>
    /// <param name="stateName">Name of the state for this grain</param>
    /// <param name="grainId">Grain ID</param>
    /// <param name="grainState">State data object to be populated for this grain.</param>
    /// <typeparam name="T">The grain state type.</typeparam>
    /// <returns>Completion promise for the Read operation on the specified grain.</returns>
    Task ReadStateAsync<T>(
        string stateName, GrainId grainId, IGrainState<T> grainState);

    /// <summary>Write data function for this storage instance.</summary>
    /// <param name="stateName">Name of the state for this grain</param>
    /// <param name="grainId">Grain ID</param>
    /// <param name="grainState">State data object to be written for this grain.</param>
    /// <typeparam name="T">The grain state type.</typeparam>
    /// <returns>Completion promise for the Write operation on the specified grain.</returns>
    Task WriteStateAsync<T>(
        string stateName, GrainId grainId, IGrainState<T> grainState);

    /// <summary>Delete / Clear data function for this storage instance.</summary>
    /// <param name="stateName">Name of the state for this grain</param>
    /// <param name="grainId">Grain ID</param>
    /// <param name="grainState">Copy of last-known state data object for this grain.</param>
    /// <typeparam name="T">The grain state type.</typeparam>
    /// <returns>Completion promise for the Delete operation on the specified grain.</returns>
    Task ClearStateAsync<T>(
        string stateName, GrainId grainId, IGrainState<T> grainState);
}
/// <summary>
/// Interface to be implemented for a storage able to read and write Orleans grain state data.
/// </summary>
public interface IGrainStorage
{
    /// <summary>Read data function for this storage instance.</summary>
    /// <param name="grainType">Type of this grain [fully qualified class name]</param>
    /// <param name="grainReference">Grain reference object for this grain.</param>
    /// <param name="grainState">State data object to be populated for this grain.</param>
    /// <returns>Completion promise for the Read operation on the specified grain.</returns>
    Task ReadStateAsync(
        string grainType, GrainReference grainReference, IGrainState grainState);

    /// <summary>Write data function for this storage instance.</summary>
    /// <param name="grainType">Type of this grain [fully qualified class name]</param>
    /// <param name="grainReference">Grain reference object for this grain.</param>
    /// <param name="grainState">State data object to be written for this grain.</param>
    /// <returns>Completion promise for the Write operation on the specified grain.</returns>
    Task WriteStateAsync(
        string grainType, GrainReference grainReference, IGrainState grainState);

    /// <summary>Delete / Clear data function for this storage instance.</summary>
    /// <param name="grainType">Type of this grain [fully qualified class name]</param>
    /// <param name="grainReference">Grain reference object for this grain.</param>
    /// <param name="grainState">Copy of last-known state data object for this grain.</param>
    /// <returns>Completion promise for the Delete operation on the specified grain.</returns>
    Task ClearStateAsync(
        string grainType, GrainReference grainReference, IGrainState grainState);
}

通过实现此接口并 注册 该实现来创建自定义存储提供程序。 有关现有存储提供程序实现的示例,请参阅 AzureBlobGrainStorage

存储提供者语义

不透明的提供程序特定Etag值(string)可以由存储提供程序设置,作为读取状态时填充的粒度状态元数据的一部分。 某些提供商可能会选择将此保留为null,如果他们不使用Etag的话。

当存储提供程序检测到 Etag 约束冲突时,任何尝试执行写入操作 都应 导致写入 Task 出错,并用临时错误包装基础存储异常。

public class InconsistentStateException : OrleansException
{
    public InconsistentStateException(
    string message,
    string storedEtag,
    string currentEtag,
    Exception storageException)
        : base(message, storageException)
    {
        StoredEtag = storedEtag;
        CurrentEtag = currentEtag;
    }

    public InconsistentStateException(
        string storedEtag,
        string currentEtag,
        Exception storageException)
        : this(storageException.Message, storedEtag, currentEtag, storageException)
    {
    }

    /// <summary>The Etag value currently held in persistent storage.</summary>
    public string StoredEtag { get; }

    /// <summary>The Etag value currently held in memory, and attempting to be updated.</summary>
    public string CurrentEtag { get; }
}

存储操作的任何其他故障条件必须导致返回Task失败,并出现指示底层存储问题的异常。 在许多情况下,此异常可能会被抛回给通过调用粒方法来触发存储操作的调用方。 请务必考虑调用方是否可以反序列化此异常。 例如,客户端可能尚未加载包含异常类型的特定持久性库。 因此,建议将异常转换为可传播回调用方的异常。

数据映射

单个存储提供程序应决定如何最好地存储粒度状态 - blob(各种格式/序列化表单)或每字段列是明显的选择。

注册存储供应商

创建粒度时,运行时 Orleans 会从服务提供商(IServiceProvider)解析存储提供程序。 运行时会解析IGrainStorage的实例。 如果存储提供程序已被命名(例如,通过 [PersistentState(stateName, storageName)] 属性),那么会解析出一个命名的 IGrainStorage 实例。

若要注册命名实例 IGrainStorage,请使用 AddSingletonNamedService 扩展方法, 请遵循此处的 AzureTableGrainStorage 提供程序的示例。