Dela via


Orleans Transaktioner

Orleans stöder distribuerade ACID-transaktioner mot beständiga korntillstånd. Transaktioner implementeras genom Microsoft.Orleans.Transactions NuGet-paketet. Källkoden för exempelappen i den här artikeln består av fyra projekt:

  • Abstraktioner: Ett klassbibliotek som innehåller gränssnitt för grain och delade klasser.
  • Korn: Ett klassbibliotek som innehåller kornimplementeringarna.
  • Server: En konsolapp som använder abstraktions- och grains-klassbibliotek och fungerar som en Orleans silo.
  • Klient: En konsolapp som använder abstraktionsklassbiblioteket som representerar Orleans klienten.

Inställningar

Orleans transaktioner är frivilliga. Både silon och klienten måste konfigureras för att använda transaktioner. Om de inte har konfigurerats får alla anrop till transaktionsmetoder på en kornimplementering ett OrleansTransactionsDisabledException. Om du vill aktivera transaktioner på en silo, anropar du SiloBuilderExtensions.UseTransactions på silovärdbyggaren:

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

På samma sätt, för att aktivera transaktioner på klienten, anropar du klientvärdbyggaren: ClientBuilderExtensions.UseTransactions

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

Transaktionstillståndslagring

Om du vill använda transaktioner måste du konfigurera ett datalager. För att stödja olika datalager med transaktioner Orleans använder du lagringsabstraktionen ITransactionalStateStorage<TState>. Den här abstraktionen är specifik för transaktionernas behov, till skillnad från allmän kornlagring (IGrainStorage). Om du vill använda transaktionsspecifik lagring konfigurerar du silon med valfri implementering av ITransactionalStateStorage, till exempel Azure (AddAzureTableTransactionalStateStorage).

Tänk till exempel på följande konfiguration av värdbyggaren:

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

I utvecklingssyfte kan du använda en IGrainStorage implementering i stället om transaktionsspecifik lagring inte är tillgänglig för det datalager du behöver. För alla transaktionstillstånd utan ett konfigurerat arkiv försöker transaktioner redundansväxla till kornlagringen med hjälp av en brygga. Det är mindre effektivt att komma åt transaktionstillstånd via en brygga till kornlagring och kanske inte stöds i framtiden. Därför rekommenderar vi att du endast använder den här metoden i utvecklingssyfte.

Gränssnitt för korn

För att ett korn ska stödja transaktioner måste du markera transaktionsmetoder på dess gränssnitt för korn som en del av en transaktion med hjälp av TransactionAttribute. Attributet måste ange hur kravanropet beter sig i en transaktionsmiljö, beskrivs med följande TransactionOption värden:

  • TransactionOption.Create: Anropet är transaktionellt och skapar alltid en ny transaktionskontext (den startar en ny transaktion), även om den anropas i en befintlig transaktionskontext.
  • TransactionOption.Join: Anropet är transaktionellt men kan bara anropas inom ramen för en befintlig transaktion.
  • TransactionOption.CreateOrJoin: Anropet är transaktionellt. Om den anropas inom ramen för en transaktion kommer den att använda den kontexten, annars skapas en ny kontext.
  • TransactionOption.Suppress: Anropet är inte transaktionellt men kan anropas inifrån en transaktion. Om den anropas inom ramen för en transaktion skickas inte kontexten till anropet.
  • TransactionOption.Supported: Anropet är inte transaktionellt men stöder transaktioner. Om den anropas inom ramen för en transaktion skickas kontexten till anropet.
  • TransactionOption.NotAllowed: Anropet är inte transaktionellt och kan inte anropas inifrån en transaktion. Om den anropas inom ramen för en transaktion kastar den NotSupportedException.

Du kan markera anrop som TransactionOption.Create, vilket innebär att anropet alltid startar transaktionen. Åtgärden i uttagsautomat nedan startar till exempel Transfer alltid en ny transaktion som omfattar de två refererade kontona.

namespace TransactionalExample.Abstractions;

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

Transaktionsåtgärderna Withdraw och Deposit på kontokornet är markerade TransactionOption.Join. Detta indikerar att de bara kan anropas inom ramen för en befintlig transaktion, vilket skulle vara fallet om det anropas under IAtmGrain.Transfer. Anropet GetBalance är markerat CreateOrJoin, så du kan anropa det antingen från en befintlig transaktion (till exempel via IAtmGrain.Transfer) eller på egen hand.

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

Viktiga överväganden

Du kan inte markera OnActivateAsync som transaktionell eftersom ett sådant anrop kräver korrekt konfiguration före anropet. Den finns bara för API:et för kornprogram. Det innebär att försök att läsa transaktionstillstånd som en del av dessa metoder genererar ett undantag i körningen.

Korniga implementeringar

En kornimplementering måste använda en ITransactionalState<TState> aspekt för att hantera korntillstånd via ACID-transaktioner.

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

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

Utför all läs- eller skrivåtkomst till det persistenta tillståndet via synkrona funktioner som skickas till den transaktionella tillståndsaspekten. Detta gör att transaktionssystemet kan utföra eller avbryta dessa åtgärder transaktionsmässigt. Om du vill använda transaktionstillstånd inom ett korn definierar du en serialiserbar tillståndsklass som ska bevaras och deklarera transaktionstillståndet i kornkonstruktorn med hjälp av en TransactionalStateAttribute. Det här attributet deklarerar tillståndsnamnet och, om du vill, vilken transaktionstillståndslagring som ska användas. Mer information finns i Installation.

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

Till exempel definieras tillståndsobjektet Balance på följande sätt:

namespace TransactionalExample.Abstractions;

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

Föregående tillståndsobjekt:

  • Är dekorerad med GenerateSerializerAttribute för att instruera Orleans kodgeneratorn att generera en serialiserare.
  • Har en Value egenskap som är dekorerad med IdAttribute för att unikt identifiera medlemmen.

Tillståndsobjektet Balance används sedan i implementeringen enligt AccountGrain följande:

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

Viktigt!

Ett transaktionellt korn måste markeras med ReentrantAttribute för att säkerställa att transaktionskontexten skickas korrekt till kornanropet.

I föregående exempel TransactionalStateAttribute deklareras att balance konstruktorparametern ska associeras med ett transaktionstillstånd med namnet "balance". Med den här deklarationen Orleans matar in en ITransactionalState<TState> instans med tillstånd som läses in från transaktionstillståndslagringen med namnet "TransactionStore". Du kan ändra tillståndet via PerformUpdate eller läsa det via PerformRead. Transaktionsinfrastrukturen säkerställer att alla sådana ändringar som utförs som en del av en transaktion (även bland flera korn som distribueras över ett Orleans kluster) antingen allokeras eller ångras när kornanropet som skapade transaktionen har slutförts (IAtmGrain.Transfer i föregående exempel).

Anropa transaktionsmetoder från en klient

Det rekommenderade sättet att anropa en transaktionell grainmetod är att använda ITransactionClient. Orleans registreras automatiskt hos beroendeinjektionstjänstleverantören när du konfigurerar ITransactionClientOrleans-klienten. Använd ITransactionClient för att skapa en transaktionskontext och anropa transaktionella grain-metoder inom den kontexten. I följande exempel visas hur du använder ITransactionClient för att anropa transaktionella kornmetoder.

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

I föregående klientkod:

  • IHostBuilder är konfigurerad med UseOrleansClient.
    • IClientBuilder Använder localhost-klustring och transaktioner.
  • Gränssnitten IClusterClient och ITransactionClient hämtas från tjänstleverantören.
  • Variablerna from och to tilldelas sina IAccountGrain referenser.
  • ITransactionClient Används för att skapa en transaktion och anropar:
    • Withdraw på kontokornsreferensen from .
    • Deposit på kontokornsreferensen to .

Transaktioner utförs alltid såvida inte ett undantag genereras i transactionDelegate eller om en motsägelse transactionOption har angetts. Även om du använder ITransactionClient är det rekommenderade sättet att anropa transaktionskornsmetoder, kan du även anropa dem direkt från ett annat korn.

Anropa transaktionsmetoder från ett annat korn

Anropa transaktionsmetoder i ett korngränssnitt som vilken annan kornmetod som helst. Som ett alternativ till att använda ITransactionClient implementerar AtmGrain nedan metoden Transfer (som är transaktionell) i IAccountGrain gränssnittet.

Överväg AtmGrain implementeringen, som hanterar de två refererade konto-enheterna och gör lämpliga anrop till Withdraw och 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));
}

Din klientappskod kan anropa AtmGrain.Transfer transaktionellt på följande sätt:

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

I föregående anrop används en IAtmGrain för att överföra 100 valutaenheter från ett konto till ett annat. När överföringen är klar kontrolleras båda kontona för att få deras nuvarande saldo. Valutaöverföringen samt båda kontofrågorna utförs som ACID-transaktioner.

Som du ser i föregående exempel kan transaktioner returnera värden inom en Task, som andra korniga anrop. Men vid anropsfel kastar de inte programfel, utan en OrleansTransactionException eller TimeoutException. Om programmet utlöser ett undantag under transaktionen och det undantaget gör att transaktionen misslyckas (i stället för att misslyckas på grund av andra systemfel) blir programundansundansen det inre undantaget för OrleansTransactionException.

Om ett transaktionsfel av typen OrleansTransactionAbortedException utlöses har transaktionen misslyckats och kan försöka igen. Alla andra undantag som utlöses indikerar att transaktionen avslutades med ett okänt tillstånd. Eftersom transaktioner är distribuerade åtgärder kan en transaktion i ett okänt tillstånd ha lyckats, misslyckats eller fortfarande pågår. Därför är det lämpligt att tillåta en tidsgräns för samtal (SiloMessagingOptions.SystemResponseTimeout) att passera innan du verifierar tillståndet eller försöker utföra åtgärden igen för att undvika sammanhängande avbrott.