Freigeben über


Kornpersistenz

Getreide kann mehrere benannte persistente Datenobjekte zugeordnet sein. Diese Zustandsobjekte werden während der Kornaktivierung aus dem Speicher geladen, sodass sie während der Anforderungen verfügbar sind. Die Grain-Persistenz verwendet ein erweiterbares Plug-in-Modell, das die Nutzung von Speicher-Providern für jede Datenbank ermöglicht. Dieses Persistenzmodell ist auf Einfachheit ausgelegt und soll nicht alle Datenzugriffsmuster abdecken. Getreide kann auch direkt auf Datenbanken zugreifen, ohne das Kornpersistenzmodell zu verwenden.

Kornpersistenzdiagramm

Im vorherigen Diagramm verfügt UserGrain über einen Profilstatus und einen Cart-Zustand , die jeweils in einem separaten Speichersystem gespeichert sind.

Ziele

  1. Unterstützt mehrere benannte persistente Datenobjekte pro Korn.
  2. Lassen Sie mehrere konfigurierte Speicheranbieter zu, die potenziell eine andere Konfiguration haben und von einem anderen Speichersystem gesichert werden.
  3. Ermöglichen Sie der Community, Speicheranbieter zu entwickeln und zu veröffentlichen.
  4. Geben Sie Speicheranbietern vollständige Kontrolle darüber, wie sie Kornzustandsdaten im permanenten Sicherungsspeicher speichern. Corollary: Orleans bietet keine umfassende ORM-Speicherlösung, ermöglicht aber benutzerdefinierten Speicheranbietern, bestimmte ORM-Anforderungen nach Bedarf zu unterstützen.

Pakete

Sie finden Orleans Getreidespeicheranbieter auf NuGet. Zu den offiziell gepflegten Paketen gehören:

Programmierschnittstelle (API)

Körner interagieren mit ihrem persistenten Zustand, wobei sie IPersistentState<TState>verwenden, bei dem TState der serialisierbare Zustandstyp ist:

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 injiziert Instanzen von IPersistentState<TState> als Konstruktorparameter in das Grain. Sie können diese Parameter mit einem PersistentStateAttribute Attribut kommentieren, um den Namen des injizierten Zustands und den Namen des Speicheranbieters zu identifizieren, der ihn angibt. Im folgenden Beispiel wird dies veranschaulicht, indem zwei benannte Zustände in den UserGrain Konstruktor eingefügt werden:

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;
    }
}

Verschiedene Getreidetypen können unterschiedliche konfigurierte Speicheranbieter verwenden, auch wenn beide denselben Typ haben (z. B. zwei verschiedene Azure Table Storage-Anbieterinstanzen, die mit verschiedenen Azure Storage-Konten verbunden sind).

Lesestatus

Der Status eines Körnchens wird automatisch erfasst, wenn es aktiviert wird. Aber die Körnchen sind dafür verantwortlich, den Schreibvorgang für jeden geänderten Status bei Bedarf explizit auszulösen.

Wenn ein Korn den aktuellen Zustand explizit aus dem Sicherungsspeicher erneut lesen möchte, sollte die ReadStateAsync Methode aufgerufen werden. Dadurch wird der Getreidezustand aus dem persistenten Speicher über den Speicheranbieter neu geladen. Die vorherige In-Memory-Kopie des Zustands des Grain wird überschrieben und ersetzt, wenn der Task von ReadStateAsync() abgeschlossen ist.

Greifen Sie mithilfe der State Eigenschaft auf den Wert des Zustands zu. Die folgende Methode greift beispielsweise auf den im Code oben deklarierten Profilstatus zu:

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

Während des normalen Betriebs ist es nicht erforderlich, ReadStateAsync() aufzurufen; Orleans lädt den Zustand während der Aktivierung automatisch. Sie können jedoch ReadStateAsync() verwenden, um den extern geänderten Zustand zu aktualisieren.

Ausführliche Informationen zu Fehlerbehandlungsmechanismen finden Sie unten im Abschnitt " Fehlermodi ".

Schreibzustand

Sie können den Zustand über die State Eigenschaft ändern. Der geänderte Zustand wird nicht automatisch beibehalten. Stattdessen entscheiden Sie, wann der Zustand beibehalten werden soll, indem Sie die WriteStateAsync Methode aufrufen. Mit der folgenden Methode wird beispielsweise eine Eigenschaft auf State aktualisiert und der aktualisierte Zustand gespeichert.

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

Konzeptionell verwendet die Orleans Runtime eine tiefe Kopie des Kornzustandsdatenobjekts für die Verwendung während aller Schreibvorgänge. Unter den Deckeln kann die Laufzeit Optimierungsregeln und Heuristiken verwenden, um die Ausführung einiger oder aller tiefen Kopien unter bestimmten Umständen zu vermeiden, vorausgesetzt, die erwartete logische Isolationsemantik wird beibehalten.

Weitere Informationen zu Fehlerbehandlungsmechanismen finden Sie im Abschnitt " Fehlermodi " weiter unten.

Status löschen

Die ClearStateAsync Methode löscht den Lagerzustand des Getreides. Je nach Anbieter kann dieser Vorgang optional den Kornzustand vollständig löschen.

Loslegen

Bevor ein Grain Datenpersistenz verwenden kann, müssen Sie einen Speicheranbieter im Silo konfigurieren.

Konfigurieren Sie zunächst Speicheranbieter, eine für den Profilstatus und eine für den Warenkorbstatus:

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();

Von Bedeutung

Microsoft empfiehlt, immer den sichersten Authentifizierungsflow zu verwenden. Wenn Sie eine Verbindung mit Azure SQL herstellen, ist Managed Identities for Azure Resources die empfohlene Authentifizierungsmethode.

Nachdem Sie nun einen Speicheranbieter mit dem Namen "profileStore" konfiguriert haben, können Sie von einem Grain aus auf diesen Anbieter zugreifen.

Sie können einem Getreide auf zwei primäre Weise beständigen Zustand hinzufügen:

  1. Durch Einfügen IPersistentState<TState> in den Konstruktor des Korns.
  2. Indem man von Grain<TGrainState> erbt

Die empfohlene Methode, einem Grain Speicher hinzuzufügen, besteht darin, IPersistentState<TState> in den Konstruktor des Grain mit einem zugehörigen [PersistentState("stateName", "providerName")]-Attribut einzuspritzen. Einzelheiten zu Grain<TState> finden Sie unter Verwenden Grain<TState> zum Hinzufügen von Speicher zu einem Korn unten. Die Verwendung Grain<TState> wird weiterhin unterstützt, gilt jedoch als legacy-Ansatz.

Deklarieren Sie eine Klasse, die den Zustand Ihres Getreides speichert.

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

    public Date DateOfBirth { get; set; }
}

IPersistentState<ProfileState> in den Konstruktor des Korns einfügen.

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

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

Von Bedeutung

Der Profilstatus wird zu dem Zeitpunkt, zu dem er in den Konstruktor eingefügt wird, nicht geladen, sodass der Zugriff darauf zu diesem Zeitpunkt ungültig ist. Der Zustand wird geladen, bevor OnActivateAsync aufgerufen wird.

Nachdem das Korn nun einen beständigen Zustand aufweist, können Sie Methoden zum Lesen und Schreiben des Zustands hinzufügen:

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();
    }
}

Fehlermodi für Persistenzvorgänge

Fehlermodi für Lesevorgänge

Fehler, die vom Speicheranbieter während des anfänglichen Lesens von Statusdaten für ein bestimmtes Korn zurückgegeben werden, schlagen beim Aktivierungsvorgang für dieses Korn fehl. In solchen Fällen gibt es keinen Aufruf der Lebenszyklusrückrufmethode dieses Korns OnActivateAsync . Die ursprüngliche Anforderung an das Korn, das die Aktivierungsfehler zurück an den Aufrufer verursacht hat, genau wie jeder andere Fehler während der Kornaktivierung. Fehler, die vom Speicheranbieter beim Lesen von Zustandsdaten für ein bestimmtes Grain auftreten, führen zu einer Ausnahme durch ReadStateAsyncTask. Der Grain kann wählen, ob er die Task Ausnahme bearbeiten oder ignorieren soll, wie jedes andere Task in Orleans.

Jeder Versuch, eine Nachricht an einen Grain zu senden, der aufgrund einer fehlenden oder fehlerhaften Speicheranbieterkonfiguration beim Silostart nicht geladen werden konnte, führt zu dem permanenten Fehler BadProviderConfigException.

Fehlermodi für Schreibvorgänge

Fehler des Speicheranbieters beim Schreiben von Zustandsdaten für einen spezifischen Grain führen zu einer Ausnahme, die von der WriteStateAsync()Task. Dies bedeutet in der Regel, dass die Kornaufruf-Ausnahme an den Clientanrufer zurückgeworfen wird, vorausgesetzt, dies WriteStateAsync()Task wird ordnungsgemäß in die endgültige Rückgabe Task für diese Kornmethode verkettet. In bestimmten erweiterten Szenarien können Sie jedoch Graincode schreiben, um solche Schreibfehler speziell zu behandeln, genau wie bei der Behandlung anderer fehlerhafter Task.

Grains, die fehlerbehandlungs- oder wiederherstellungscode ausführen , müssen Ausnahmen oder fehlerhafte WriteStateAsync()Tasks abfangen und nicht erneut auslösen, was bedeutet, dass sie den Schreibfehler erfolgreich behandelt haben.

Empfehlungen

Verwenden der JSON-Serialisierung oder eines anderen versionstoleranten Serialisierungsformats

Code wird weiterentwickelt, und dies umfasst häufig Speichertypen. Um diese Änderungen zu berücksichtigen, konfigurieren Sie einen geeigneten Serialisierer. Für die meisten Speicheranbieter steht eine UseJson Option oder ein ähnliches Format zur Verwendung von JSON als Serialisierungsformat zur Verfügung. Stellen Sie sicher, dass beim Entwickeln von Datenverträgen bereits gespeicherte Daten weiterhin geladen werden können.

Verwenden von Grain<TState> zum Hinzufügen von Speicher zu einem Grain

Von Bedeutung

Die Verwendung von Grain<T> zum Hinzufügen von Speicher zu einem Speichergranulat gilt als veraltete Funktionalität. Fügen Sie Getreidespeicher wie zuvor beschrieben unter Verwendung von IPersistentState<T> hinzu.

Grain-Klassen, die von Grain<T> erben (wobei T ein anwendungsspezifischer Zustandsdatentyp ist, der Persistenz benötigt), laden ihren Zustand automatisch aus dem angegebenen Speicher.

Markieren Sie solche Körner mit einer Angabe einer StorageProviderAttribute benannten Instanz eines Speicheranbieters, die zum Lesen/Schreiben der Statusdaten für dieses Korn verwendet werden soll.

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

Die Grain<T> Basisklasse definiert die folgenden Methoden für Unterklassen, die aufgerufen werden sollen:

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

Das Verhalten dieser Methoden entspricht ihren zuvor definierten Gegenstücken auf IPersistentState<TState>.

Erstellen eines Speicheranbieters

Es gibt zwei Teile der Zustandspersistenz-APIs: die API, die dem Korn über IPersistentState<T> oder Grain<T> bereitgestellt wird, und die Speicheranbieter-API, die sich um IGrainStorage zentriert—die Schnittstelle, die Speicheranbieter implementieren müssen.

/// <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);
}

Erstellen Sie einen benutzerdefinierten Speicheranbieter, indem Sie diese Schnittstelle implementieren und diese Implementierung registrieren . Ein Beispiel für eine vorhandene Speicheranbieterimplementierung finden Sie unter AzureBlobGrainStorage.

Speicheranbietersemantik

Ein undurchsichtiger anbieterspezifischer Etag Wert (string) kann von einem Speicheranbieter als Teil der Kornstatusmetadaten festgelegt werden, die beim Lesen des Zustands aufgefüllt wurden. Einige Anbieter können dies als null belassen, wenn sie Etag nicht verwenden.

Jeder Versuch, eine Schreiboperation auszuführen, wenn der Speicheranbieter eine Etag Constraint-Verletzung erkennt , sollte dazu führen, dass der Schreibvorgang Task mit einem vorübergehenden Fehler InconsistentStateException fehlschlägt und die zugrunde liegende Speicherausnahme umschlossen wird.

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; }
}

Alle anderen Fehlerbedingungen eines Speichervorgangs müssen dazu führen, dass der zurückgegebene Task durch eine Ausnahme aufgelöst wird, die das zugrunde liegende Speicherproblem angibt. In vielen Fällen kann diese Ausnahme an den Aufrufer zurückgegeben werden, der den Speichervorgang ausgelöst hat, indem eine Methode auf dem Grain aufgerufen wird. Es ist wichtig zu überlegen, ob der Aufrufer diese Ausnahme deserialisieren kann. Beispielsweise hat der Client möglicherweise nicht die spezifische Persistenzbibliothek geladen, die den Ausnahmetyp enthält. Aus diesem Grund ist es ratsam, Ausnahmen in Ausnahmen umzuwandeln, die an den Aufrufer zurückverbreitet werden können.

Datenabbildung

Einzelne Speicheranbieter sollten entscheiden, wie der Kornzustand am besten gespeichert werden soll – Blob (verschiedene Formate/ serialisierte Formulare) oder Spalten pro Feld sind offensichtliche Auswahlmöglichkeiten.

Registrieren eines Speicheranbieters

Die Orleans Laufzeit löst einen Speicheranbieter vom Dienstanbieter (IServiceProvider) auf, wenn ein Korn erstellt wird. Die Laufzeit löst eine Instanz von IGrainStorage. Wenn der Speicheranbieter benannt ist (z. B. über das [PersistentState(stateName, storageName)] Attribut), wird eine benannte Instanz IGrainStorage aufgelöst.

Um eine benannte Instanz von IGrainStoragezu registrieren, verwenden Sie die AddSingletonNamedService Erweiterungsmethode nach dem Beispiel des AzureTableGrainStorage-Anbieters hier.