Freigeben über


Orleans Transaktionen

Orleans unterstützt verteilte ACID-Transaktionen gegen beständigen Kornzustand. Transaktionen werden mithilfe von MicrosoftOrleans implementiert. Transaktionen NuGet-Paket. Der Quellcode für die Beispiel-App in diesem Artikel besteht aus vier Projekten:

  • Abstraktionen: Eine Klassenbibliothek, die die Kornschnittstellen und freigegebenen Klassen enthält.
  • Körner: Eine Klassenbibliothek, die die Kornimplementierungen enthält.
  • Server: Eine Konsolen-App, die die Abstraktionen und Kornklassenbibliotheken nutzt und als Orleans Silo fungiert.
  • Client: Eine Konsolen-App, die die Klassenbibliothek der Abstraktionen verwendet, die den Orleans Client darstellt.

Konfiguration

Orleans Transaktionen erfordern eine ausdrückliche Zustimmung. Sowohl das Silo als auch der Client müssen für die Verwendung von Transaktionen konfiguriert werden. Wenn sie nicht konfiguriert sind, erhält jeder Aufruf von Transaktionsmethoden auf einer Grain-Implementierung ein OrleansTransactionsDisabledException. Um Transaktionen in einem Silo zu ermöglichen, rufen Sie SiloBuilderExtensions.UseTransactions den Silohost-Generator auf:

var builder = Host.CreateDefaultBuilder(args)
    .UseOrleans((context, siloBuilder) =>
    {
        siloBuilder.UseTransactions();
    });

Um Transaktionen auf dem Client zu aktivieren, rufen Sie ClientBuilderExtensions.UseTransactions auf dem Client-Host-Builder auf.

var builder = Host.CreateDefaultBuilder(args)
    .UseOrleansClient((context, clientBuilder) =>
    {
        clientBuilder.UseTransactions();
    });

Transaktionsstatusspeicher

Um Transaktionen zu verwenden, müssen Sie einen Datenspeicher konfigurieren. Um verschiedene Datenspeicher mit Transaktionen zu unterstützen, wird die Speicherabstraktion Orleans von ITransactionalStateStorage<TState> verwendet. Diese Abstraktion ist spezifisch für die Anforderungen von Transaktionen, im Gegensatz zu generischem Getreidespeicher (IGrainStorage). Um transaktionsspezifischen Speicher zu verwenden, konfigurieren Sie das Silo mithilfe einer beliebigen Implementierung von ITransactionalStateStorage, z. B. Azure (AddAzureTableTransactionalStateStorage).

Betrachten Sie beispielsweise die folgende Host-Generator-Konfiguration:

await Host.CreateDefaultBuilder(args)
    .UseOrleans((_, silo) =>
    {
        silo.UseLocalhostClustering();

        if (Environment.GetEnvironmentVariable(
                "ORLEANS_STORAGE_CONNECTION_STRING") is { } connectionString)
        {
            silo.AddAzureTableTransactionalStateStorage(
                "TransactionStore", 
                options => options.ConfigureTableServiceClient(connectionString));
        }
        else
        {
            silo.AddMemoryGrainStorageAsDefault();
        }

        silo.UseTransactions();
    })
    .RunConsoleAsync();

Wenn transaktionsspezifischer Speicher für den benötigten Datenspeicher nicht verfügbar ist, können Sie stattdessen eine IGrainStorage Implementierung verwenden. Bei jedem transaktionalen Zustand ohne konfigurierten Speicher versuchen die Transaktionen, auf den Grainspeicher mittels einer Bridge überzugehen. Der Zugriff auf den Transaktionsstatus über eine Brücke zum Getreidespeicher ist weniger effizient und wird in Zukunft möglicherweise nicht unterstützt. Daher empfehlen wir, diesen Ansatz nur für Entwicklungszwecke zu verwenden.

Kornschnittstellen

Damit ein Grain Transaktionen unterstützt, müssen Sie transaktionale Methoden in seiner Grain-Schnittstelle als Teil einer Transaktion mit dem TransactionAttribute kennzeichnen. Das Attribut muss angeben, wie sich der Grain-Call in einer Transaktionsumgebung verhält, wie in den folgenden TransactionOption Werten beschrieben:

  • TransactionOption.Create: Der Aufruf ist transaktionsal und erstellt immer einen neuen Transaktionskontext (er startet eine neue Transaktion), auch wenn er innerhalb eines vorhandenen Transaktionskontexts aufgerufen wird.
  • TransactionOption.Join: Der Aufruf ist transaktionsal, kann aber nur im Kontext einer vorhandenen Transaktion aufgerufen werden.
  • TransactionOption.CreateOrJoin: Der Anruf ist transaktional. Wenn sie im Kontext einer Transaktion aufgerufen wird, wird dieser Kontext verwendet, andernfalls wird ein neuer Kontext erstellt.
  • TransactionOption.Suppress: Der Anruf ist nicht transaktionsal, kann aber innerhalb einer Transaktion aufgerufen werden. Wird ein Aufruf innerhalb des Kontexts einer Transaktion durchgeführt, wird der Kontext nicht an den Aufruf übergeben.
  • TransactionOption.Supported: Der Anruf ist nicht transaktional, sondern unterstützt Transaktionen. Wenn im Rahmen einer Transaktion aufgerufen, wird der Kontext an den Aufruf übergeben.
  • TransactionOption.NotAllowed: Der Anruf ist nicht transaktional und kann nicht innerhalb einer Transaktion aufgerufen werden. Wenn sie im Kontext einer Transaktion aufgerufen wird, löst dies die NotSupportedException.

Sie können Aufrufe als TransactionOption.Createkennzeichnen, was bedeutet, dass der Aufruf immer seine Transaktion startet. Beispielsweise startet der Transfer Vorgang im unten gezeigten ATM-Bereich immer eine neue Transaktion, die die beiden referenzierten Konten einbezieht.

namespace TransactionalExample.Abstractions;

public interface IAtmGrain : IGrainWithIntegerKey
{
    [Transaction(TransactionOption.Create)]
    Task Transfer(string fromId, string toId, decimal amountToTransfer);
}

Die Transaktionsvorgänge Withdraw und Deposit auf dem Kontogetriebe sind als TransactionOption.Join gekennzeichnet. Dies weist darauf hin, dass sie nur im Kontext einer vorhandenen Transaktion aufgerufen werden können. Dies wäre der Fall, wenn sie während IAtmGrain.Transferdes Vorgangs aufgerufen werden. Der GetBalance Anruf ist markiert CreateOrJoin, sodass Sie ihn entweder innerhalb einer vorhandenen Transaktion (z. B. via IAtmGrain.Transfer) oder eigenständig aufrufen können.

namespace TransactionalExample.Abstractions;

public interface IAccountGrain : IGrainWithStringKey
{
    [Transaction(TransactionOption.Join)]
    Task Withdraw(decimal amount);

    [Transaction(TransactionOption.Join)]
    Task Deposit(decimal amount);

    [Transaction(TransactionOption.CreateOrJoin)]
    Task<decimal> GetBalance();
}

Wichtige Überlegungen

Sie können OnActivateAsync nicht als transaktional kennzeichnen, da ein solcher Aufruf vorher ordnungsgemäß eingerichtet werden muss. Es ist nur für die Kornanwendungs-API vorhanden. Dies bedeutet, dass der Versuch, den Transaktionsstatus im Rahmen dieser Methoden zu lesen, eine Ausnahme in der Laufzeit auslöst.

Getreideimplementierungen

Eine Getreideimplementierung muss ein ITransactionalState<TState> Facet verwenden, um den Getreidezustand über ACID-Transaktionen zu verwalten.

public interface ITransactionalState<TState>
    where TState : class, new()
{
    Task<TResult> PerformRead<TResult>(
        Func<TState, TResult> readFunction);

    Task<TResult> PerformUpdate<TResult>(
        Func<TState, TResult> updateFunction);
}

Führen Sie den gesamten Lese- oder Schreibzugriff auf den permanenten Zustand über synchrone Funktionen aus, die an das Facet "Transaktionsstatus" übergeben werden. Auf diese Weise kann das Transaktionssystem diese Vorgänge transaktional ausführen oder abbrechen. Um den Transaktionsstatus innerhalb eines Korns zu verwenden, definieren Sie eine serialisierbare Zustandsklasse, die beibehalten werden soll, und deklarieren Sie den Transaktionsstatus im Konstruktor des Korns mithilfe eines TransactionalStateAttribute. Dieses Attribut deklariert den Statusnamen und optional den zu verwendenden Transaktionsstatusspeicher. Weitere Informationen finden Sie unter Setup.

[AttributeUsage(AttributeTargets.Parameter)]
public class TransactionalStateAttribute : Attribute
{
    public TransactionalStateAttribute(string stateName, string storageName = null)
    {
        // ...
    }
}

Als Beispiel wird das Balance Statusobjekt wie folgt definiert:

namespace TransactionalExample.Abstractions;

[GenerateSerializer]
public record class Balance
{
    [Id(0)]
    public decimal Value { get; set; } = 1_000;
}

Das vorangehende Zustandsobjekt:

  • Wird mit GenerateSerializerAttribute dekoriert, um den Orleans Codegenerator anzuweisen, einen Serialisierer zu generieren.
  • Verfügt über eine Value Eigenschaft, die mit dem IdAttribute versehen ist, um das Mitglied eindeutig zu identifizieren.

Das Balance Statusobjekt wird dann wie folgt in der AccountGrain Implementierung verwendet:

namespace TransactionalExample.Grains;

[Reentrant]
public class AccountGrain : Grain, IAccountGrain
{
    private readonly ITransactionalState<Balance> _balance;

    public AccountGrain(
        [TransactionalState(nameof(balance))]
        ITransactionalState<Balance> balance) =>
        _balance = balance ?? throw new ArgumentNullException(nameof(balance));

    public Task Deposit(decimal amount) =>
        _balance.PerformUpdate(
            balance => balance.Value += amount);

    public Task Withdraw(decimal amount) =>
        _balance.PerformUpdate(balance =>
        {
            if (balance.Value < amount)
            {
                throw new InvalidOperationException(
                    $"Withdrawing {amount} credits from account " +
                    $"\"{this.GetPrimaryKeyString()}\" would overdraw it." +
                    $" This account has {balance.Value} credits.");
            }

            balance.Value -= amount;
        });

    public Task<decimal> GetBalance() =>
        _balance.PerformRead(balance => balance.Value);
}

Von Bedeutung

Ein Transaktionskorn muss mit der ReentrantAttribute Kennzeichnung gekennzeichnet werden, um sicherzustellen, dass der Transaktionskontext ordnungsgemäß an den Kornaufruf übergeben wird.

Im vorherigen Beispiel wird deklariert TransactionalStateAttribute , dass der balance Konstruktorparameter einem transaktionsbezogenen Zustand mit dem Namen "balance"zugeordnet werden soll. Mit dieser Deklaration injiziert Orleans eine ITransactionalState<TState>-Instanz mit einem Zustand, der aus dem Transaktionsstatusspeicher mit dem Namen "TransactionStore" geladen wurde. Sie können den Zustand über PerformUpdate ändern oder ihn über PerformRead lesen. Die Transaktionsinfrastruktur stellt sicher, dass alle Änderungen, die als Teil einer Transaktion ausgeführt werden (auch bei mehreren Grains, die über einen Orleans Cluster verteilt sind), entweder vollständig bestätigt oder vollständig rückgängig gemacht werden, sobald der Grain-Aufruf abgeschlossen ist, der die Transaktion erstellt hat (IAtmGrain.Transfer im vorherigen Beispiel).

Transaktionsmethoden von einem Client aufrufen

Die empfohlene Methode zum Aufrufen einer Transaktions-Grain-Methode ist die Nutzung von ITransactionClient. Orleans registriert ITransactionClient automatisch beim Abhängigkeitsinjektions-Dienstanbieter, wenn Sie den Orleans-Client konfigurieren. Verwenden Sie ITransactionClient, um einen Transaktionskontext zu erstellen und Transaktions-Grain-Methoden innerhalb dieses Kontexts aufzurufen. Das folgende Beispiel zeigt, wie ITransactionClient zum Aufrufen von Transaktions-Grain-Methoden verwendet wird.

using IHost host = Host.CreateDefaultBuilder(args)
    .UseOrleansClient((_, client) =>
    {
        client.UseLocalhostClustering()
            .UseTransactions();
    })
    .Build();

await host.StartAsync();

var client = host.Services.GetRequiredService<IClusterClient>();
var transactionClient= host.Services.GetRequiredService<ITransactionClient>();

var accountNames = new[] { "Xaawo", "Pasqualino", "Derick", "Ida", "Stacy", "Xiao" };
var random = Random.Shared;

while (!Console.KeyAvailable)
{
    // Choose some random accounts to exchange money
    var fromIndex = random.Next(accountNames.Length);
    var toIndex = random.Next(accountNames.Length);
    while (toIndex == fromIndex)
    {
        // Avoid transferring to/from the same account, since it would be meaningless
        toIndex = (toIndex + 1) % accountNames.Length;
    }

    var fromKey = accountNames[fromIndex];
    var toKey = accountNames[toIndex];
    var fromAccount = client.GetGrain<IAccountGrain>(fromKey);
    var toAccount = client.GetGrain<IAccountGrain>(toKey);

    // Perform the transfer and query the results
    try
    {
        var transferAmount = random.Next(200);

        await transactionClient.RunTransaction(
            TransactionOption.Create, 
            async () =>
            {
                await fromAccount.Withdraw(transferAmount);
                await toAccount.Deposit(transferAmount);
            });

        var fromBalance = await fromAccount.GetBalance();
        var toBalance = await toAccount.GetBalance();

        Console.WriteLine(
            $"We transferred {transferAmount} credits from {fromKey} to " +
            $"{toKey}.\n{fromKey} balance: {fromBalance}\n{toKey} balance: {toBalance}\n");
    }
    catch (Exception exception)
    {
        Console.WriteLine(
            $"Error transferring credits from " +
            $"{fromKey} to {toKey}: {exception.Message}");

        if (exception.InnerException is { } inner)
        {
            Console.WriteLine($"\tInnerException: {inner.Message}\n");
        }

        Console.WriteLine();
    }

    // Sleep and run again
    await Task.Delay(TimeSpan.FromMilliseconds(200));
}

Im vorherigen Clientcode:

  • Die IHostBuilder ist mit UseOrleansClient konfiguriert.
    • Dies IClientBuilder verwendet localhost-Clustering und Transaktionen.
  • Die IClusterClient- und ITransactionClient-Schnittstellen werden vom Dienstanbieter abgerufen.
  • Die Variablen from und to werden ihren IAccountGrain Verweisen zugewiesen.
  • Dies ITransactionClient wird verwendet, um eine Transaktion zu erstellen, wobei Folgendes aufgerufen wird:
    • Withdraw auf dem Kontokornverweis from .
    • Deposit auf dem Kontokornverweis to .

Transaktionen werden grundsätzlich durchgeführt, es sei denn, eine Ausnahme wird im transactionDelegate ausgelöst oder ein widersprüchlicher transactionOption angegeben ist. Während die Verwendung von ITransactionClient die empfohlene Methode zum Aufrufen von transaktionalen Grain-Methoden ist, können Sie sie auch direkt aus einem anderen Grain aufrufen.

Aufrufen von Transaktionsmethoden aus einem anderen Grain

Rufen Sie transaktionsbasierte Methoden in einer Grain-Schnittstelle wie bei jeder anderen Grain-Methode auf. Als Alternative zur Verwendung ITransactionClientruft die AtmGrain folgende Implementierung die Transfer Methode (transaktional) auf der IAccountGrain Schnittstelle auf.

Berücksichtigen Sie die AtmGrain Implementierung, die die beiden referenzierten Konto-Objekte auflöst und die entsprechenden Aufrufe an Withdraw und Deposit tätigt:

namespace TransactionalExample.Grains;

[StatelessWorker]
public class AtmGrain : Grain, IAtmGrain
{
    public Task Transfer(
        string fromId,
        string toId,
        decimal amount) =>
        Task.WhenAll(
            GrainFactory.GetGrain<IAccountGrain>(fromId).Withdraw(amount),
            GrainFactory.GetGrain<IAccountGrain>(toId).Deposit(amount));
}

Ihr Client-Anwendungscode kann AtmGrain.Transfer transaktional wie folgt aufrufen:

IAtmGrain atmOne = client.GetGrain<IAtmGrain>(0);

Guid from = Guid.NewGuid();
Guid to = Guid.NewGuid();

await atmOne.Transfer(from, to, 100);

uint fromBalance = await client.GetGrain<IAccountGrain>(from).GetBalance();
uint toBalance = await client.GetGrain<IAccountGrain>(to).GetBalance();

In den vorangehenden Anrufen wird eine IAtmGrain Verwendet, um 100 Währungseinheiten von einem Konto auf ein anderes zu übertragen. Nach Abschluss der Übertragung werden beide Konten abgefragt, um ihren aktuellen Saldo zu erhalten. Die Währungsübertragung sowie beide Kontoabfragen werden als ACID-Transaktionen ausgeführt.

Wie im vorstehenden Beispiel gezeigt, können Transaktionen Werte in einem Task zurückgeben, genau wie andere Grain-Aufrufe. Beim Aufrufversagen werfen sie jedoch keine Anwendungsausnahmen, sondern eine OrleansTransactionException oder TimeoutException. Wenn die Anwendung während der Transaktion eine Ausnahme auslöst und diese Ausnahme bewirkt, dass die Transaktion fehlschlägt (im Gegensatz zu fehlern aufgrund anderer Systemfehler), wird die Anwendungs ausnahme zur inneren Ausnahme der OrleansTransactionException.

Wenn eine Transaktions ausnahme des Typs OrleansTransactionAbortedException ausgelöst wird, ist die Transaktion fehlgeschlagen und kann wiederholt werden. Jede andere Ausnahme, die ausgelöst wird, gibt an, dass die Transaktion mit einem unbekannten Zustand beendet wurde. Da Transaktionen verteilte Vorgänge sind, könnte eine Transaktion in einem unbekannten Zustand erfolgreich, fehlgeschlagen oder noch in Bearbeitung sein. Aus diesem Grund empfiehlt es sich, ein Anruf-Timeout-Intervall (SiloMessagingOptions.SystemResponseTimeout) zuzulassen, bevor der Zustand überprüft oder der Vorgang erneut versucht wird, um kaskadierende Abbrüche zu vermeiden.