Delen via


Orleans transacties

Orleans ondersteunt gedistribueerde ACID-transacties ten opzichte van permanente graanstatus. Transacties worden geïmplementeerd met behulp van Microsoft .Orleans. NuGet-pakket transacties . De broncode voor de voorbeeld-app in dit artikel bestaat uit vier projecten:

  • Abstracties: Een klassebibliotheek met de graaninterfaces en gedeelde klassen.
  • Korrels: Een klassebibliotheek met de graanuitvoeringen.
  • Server: Een console-app die de abstracties en korrelklassebibliotheken gebruikt en fungeert als de Orleans silo.
  • Client: een console-app die gebruikmaakt van de abstractiesklassebibliotheek die de Orleans client vertegenwoordigt.

Instellingen

Orleans transacties zijn opt-in. Een silo en client moeten beide worden geconfigureerd voor het gebruik van transacties. Als ze niet zijn geconfigureerd, ontvangen aanroepen naar transactionele methoden voor een graan implementatie de OrleansTransactionsDisabledException. Als u transacties in een silo wilt inschakelen, roept u SiloBuilderExtensions.UseTransactions de silohostbouwer aan:

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

Als u transacties op de client wilt inschakelen, roept ClientBuilderExtensions.UseTransactions u de opbouwfunctie van de clienthost aan:

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

Transactionele statusopslag

Als u transacties wilt gebruiken, moet u een gegevensarchief configureren. Ter ondersteuning van verschillende gegevensarchieven met transacties wordt de opslagabstractie ITransactionalStateStorage<TState> gebruikt. Deze abstractie is specifiek voor de behoeften van transacties, in tegenstelling tot algemene graanopslag (IGrainStorage). Als u transactiespecifieke opslag wilt gebruiken, configureert u de silo met behulp van een implementatie van ITransactionalStateStorage, zoals Azure (AddAzureTableTransactionalStateStorage).

Denk bijvoorbeeld aan de volgende configuratie van de hostbouwer:

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

Als transactiespecifieke opslag niet beschikbaar is voor het gegevensarchief dat u nodig hebt, kunt u in plaats daarvan een IGrainStorage implementatie gebruiken. Voor elke transactionele status waarvoor geen archief is geconfigureerd, proberen transacties een failover uit te voeren naar de graanopslag met behulp van een brug. Het openen van een transactionele status via een brug naar graanopslag is minder efficiënt en wordt in de toekomst mogelijk niet ondersteund. Daarom is het raadzaam om dit alleen te gebruiken voor ontwikkelingsdoeleinden.

Graaninterfaces

Voor een korrel ter ondersteuning van transacties moeten transactionele methoden op een graaninterface worden gemarkeerd als onderdeel van een transactie met behulp van de TransactionAttribute. Het kenmerk moet aangeven hoe de grain-aanroep zich gedraagt in een transactionele omgeving, zoals wordt beschreven met de volgende TransactionOption waarden:

  • TransactionOption.Create: De aanroep is transactioneel en maakt altijd een nieuwe transactiecontext (er wordt een nieuwe transactie gestart), zelfs als deze wordt aangeroepen binnen een bestaande transactiecontext.
  • TransactionOption.Join: De aanroep is transactioneel, maar kan alleen worden aangeroepen binnen de context van een bestaande transactie.
  • TransactionOption.CreateOrJoin: Oproep is transactioneel. Als deze wordt aangeroepen binnen de context van een transactie, wordt die context gebruikt, anders wordt er een nieuwe context gemaakt.
  • TransactionOption.Suppress: Oproep is niet transactioneel, maar kan vanuit een transactie worden aangeroepen. Als deze wordt aangeroepen binnen de context van een transactie, wordt de context niet doorgegeven aan de aanroep.
  • TransactionOption.Supported: Oproep is niet transactioneel, maar ondersteunt transacties. Als deze wordt aangeroepen binnen de context van een transactie, wordt de context doorgegeven aan de aanroep.
  • TransactionOption.NotAllowed: Oproep is niet transactioneel en kan niet worden aangeroepen vanuit een transactie. Als deze wordt aangeroepen binnen de context van een transactie, wordt de NotSupportedException.

Oproepen kunnen worden gemarkeerd als TransactionOption.Create, wat betekent dat de aanroep altijd de transactie start. De bewerking in het onderstaande ATM-graan begint bijvoorbeeld Transfer altijd met een nieuwe transactie waarbij wordt verwezen naar de twee accounts waarnaar wordt verwezen.

namespace TransactionalExample.Abstractions;

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

De transactionele bewerkingen Withdraw en Deposit op de accountkorrel worden gemarkeerd TransactionOption.Join, waarmee wordt aangegeven dat ze alleen kunnen worden aangeroepen binnen de context van een bestaande transactie, wat het geval zou zijn als ze werden aangeroepen tijdens IAtmGrain.Transfer. De GetBalance aanroep wordt gemarkeerd CreateOrJoin zodat deze kan worden aangeroepen vanuit een bestaande transactie, zoals via IAtmGrain.Transfer, of zelfstandig.

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

Belangrijke aandachtspunten

Het OnActivateAsync kan niet worden gemarkeerd als transactioneel omdat voor een dergelijke aanroep een juiste instelling is vereist voordat de aanroep wordt uitgevoerd. Deze bestaat alleen voor de grain application-API. Dit betekent dat een poging om transactionele status te lezen als onderdeel van deze methoden een uitzondering genereert in de runtime.

Grain-implementaties

Een grain-implementatie moet een ITransactionalState<TState> facet gebruiken om de graanstatus te beheren via ACID-transacties.

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

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

Alle lees- of schrijftoegang tot de persistente status moet worden uitgevoerd via synchrone functies die worden doorgegeven aan de transactionele status facet. Hierdoor kan het transactiesysteem deze bewerkingen transactioneel uitvoeren of annuleren. Als u een transactionele status binnen een korrel wilt gebruiken, definieert u een serialiseerbare statusklasse die moet worden persistent gemaakt en declareert u de transactionele status in de constructor van het graan met een TransactionalStateAttribute. De laatste declareert de statusnaam en, optioneel, welke transactionele statusopslag moet worden gebruikt. Zie Setup voor meer informatie.

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

Als voorbeeld wordt het Balance statusobject als volgt gedefinieerd:

namespace TransactionalExample.Abstractions;

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

Het voorgaande statusobject:

  • Is ingericht met de GenerateSerializerAttribute opdracht om de Orleans codegenerator te instrueren om een serializer te genereren.
  • Heeft een Value eigenschap die is ingericht met het IdAttribute om het lid uniek te identificeren.

Het Balance statusobject wordt vervolgens als volgt in de AccountGrain implementatie gebruikt:

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

Belangrijk

Een transactionele korrel moet worden gemarkeerd met de ReentrantAttribute functie om ervoor te zorgen dat de transactiecontext correct wordt doorgegeven aan de korreloproep.

In het voorgaande voorbeeld wordt de TransactionalStateAttribute parameter gebruikt om aan te geven dat de balance constructorparameter moet worden gekoppeld aan een transactionele status met de naam "balance". Met deze declaratie Orleans injecteert u een ITransactionalState<TState> exemplaar met een status die is geladen vanuit de transactionele statusopslag met de naam "TransactionStore". De status kan worden gewijzigd via PerformUpdate of gelezen via PerformRead. De transactieinfrastructuur zorgt ervoor dat dergelijke wijzigingen die worden uitgevoerd als onderdeel van een transactie, zelfs tussen meerdere korrels die zijn verdeeld over een Orleans cluster, allemaal worden doorgevoerd of allemaal ongedaan worden gemaakt na voltooiing van de korrelaanroep die de transactie heeft gemaakt (IAtmGrain.Transfer in het voorgaande voorbeeld).

Transactiemethoden aanroepen vanaf een client

De aanbevolen manier om een transactiekorrelmethode aan te roepen, is het gebruik van de ITransactionClient. De ITransactionClient wordt automatisch geregistreerd bij de provider van de afhankelijkheidsinjectieservice wanneer de Orleans client is geconfigureerd. Deze ITransactionClient wordt gebruikt om een transactiecontext te maken en transactionele graanmethoden binnen die context aan te roepen. In het volgende voorbeeld ziet u hoe u transactionele ITransactionClient graanmethoden kunt aanroepen.

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

In de voorgaande clientcode:

  • De IHostBuilder is geconfigureerd met UseOrleansClient.
    • Het IClientBuilder maakt gebruik van localhost-clustering en transacties.
  • De IClusterClient interfaces worden ITransactionClient opgehaald van de serviceprovider.
  • De from en to variabelen worden hun verwijzingen toegewezen IAccountGrain .
  • Deze ITransactionClient wordt gebruikt om een transactie te maken, aanroepen:
    • Withdraw op de naslaginformatie over accountkorrels from .
    • Deposit op de naslaginformatie over accountkorrels to .

Transacties worden altijd doorgevoerd, tenzij er een uitzondering is die wordt gegenereerd in de transactionDelegate of tegenstrijdige transactionOption opgegeven. Hoewel de aanbevolen manier om transactionele graanmethoden aan te roepen is om de ITransactionClientmethode te gebruiken, kunt u ook transactionele graanmethoden rechtstreeks vanuit een ander graan aanroepen.

Transactiemethoden aanroepen vanuit een ander graan

Transactionele methoden op een graaninterface worden net als elke andere graanmethode aangeroepen. Als alternatieve benadering met behulp van de ITransactionClientAtmGrain onderstaande implementatie wordt de Transfer methode (transactioneel) op de IAccountGrain interface aangeroepen.

Overweeg de AtmGrain implementatie, die de twee accountkorrels waarnaar wordt verwezen, oplost en de juiste aanroepen doet naar Withdraw en Deposit:

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

De code van uw client-app kan als volgt op transactionele wijze worden aangeroepen AtmGrain.Transfer :

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 de voorgaande aanroepen wordt een IAtmGrain waarde gebruikt om 100 valuta-eenheden van het ene account naar het andere over te brengen. Nadat de overdracht is voltooid, worden beide accounts opgevraagd om hun huidige saldo te verkrijgen. De valutaoverdracht, evenals beide accountquery's, worden uitgevoerd als ACID-transacties.

Zoals in het voorgaande voorbeeld wordt weergegeven, kunnen transacties waarden retourneren binnen een Task, zoals andere graanoproepen. Maar bij een aanroepfout genereren ze geen toepassingsuitzonderingen, maar wel een OrleansTransactionException of TimeoutException. Als de toepassing een uitzondering genereert tijdens de transactie en die uitzondering ervoor zorgt dat de transactie mislukt (in tegenstelling tot mislukt vanwege andere systeemfouten), is de toepassingsuitzondering de interne uitzondering van de OrleansTransactionException.

Als er een transactie-uitzondering van het type OrleansTransactionAbortedExceptionwordt gegenereerd, kan de transactie niet worden uitgevoerd en opnieuw worden geprobeerd. Elke andere uitzondering die is opgetreden, geeft aan dat de transactie is beëindigd met een onbekende status. Omdat transacties gedistribueerde bewerkingen zijn, kan een transactie in een onbekende status zijn geslaagd, mislukt of nog steeds worden uitgevoerd. Daarom is het raadzaam om een time-outperiode voor aanroepen toe te staan (SiloMessagingOptions.SystemResponseTimeout) om trapsgewijze aborts te voorkomen, voordat u de status verifieert of de bewerking opnieuw probeert uit te voeren.