다음을 통해 공유


Orleans 트랜잭션

Orleans 는 영구 그레인 상태에 대해 분산 ACID 트랜잭션을 지원합니다. Orleans NuGet 패키지를 사용하여 트랜잭션이 구현됩니다. 이 문서의 샘플 앱에 대한 소스 코드는 다음 네 개의 프로젝트로 구성됩니다.

  • 추상화: 그레인 인터페이스 및 공유 클래스를 포함하는 클래스 라이브러리입니다.
  • Grains: 그레인 구현을 포함하는 클래스 라이브러리입니다.
  • 서버: 추상화 및 곡물 클래스 라이브러리를 사용하고 사일로 역할을 하는 Orleans 콘솔 앱입니다.
  • 클라이언트: Orleans 클라이언트를 나타내는 추상화 클래스 라이브러리를 사용하는 콘솔 앱입니다.

설치

Orleans 트랜잭션은 옵트인됩니다. 트랜잭션을 사용하도록 사일로와 클라이언트를 모두 구성해야 합니다. 구성되지 않은 경우 그레인 구현에서 트랜잭션 메서드에 대한 모든 호출이 수신됩니다 OrleansTransactionsDisabledException. 사일로에서 트랜잭션을 사용하도록 설정하려면 사일로 호스트 빌더에서 SiloBuilderExtensions.UseTransactions을 호출하십시오.

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

마찬가지로 클라이언트에서 트랜잭션을 사용하도록 설정하려면 클라이언트 호스트 작성기를 호출 ClientBuilderExtensions.UseTransactions 합니다.

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

트랜잭션 상태 스토리지

트랜잭션을 사용하려면 데이터 저장소를 구성해야 합니다. 트랜잭션을 사용하여 다양한 데이터 저장소를 Orleans 지원하려면 스토리지 추상화 ITransactionalStateStorage<TState>를 사용합니다. 이 추상화는 일반 곡물 스토리지(IGrainStorage)와 달리 트랜잭션의 요구 사항에 따라 다릅니다. 트랜잭션별 스토리지를 사용하려면 ITransactionalStateStorage 구현을 사용하여 사일로를 구성하세요. 여기에는 Azure(AddAzureTableTransactionalStateStorage)와 같은 구현이 포함됩니다.

예를 들어 다음 호스트 작성기 구성을 고려합니다.

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

개발을 위해 필요한 데이터 저장소에 트랜잭션별 스토리지를 사용할 수 없는 경우 대신 구현을 IGrainStorage 사용할 수 있습니다. 구성된 저장소가 없는 트랜잭션 상태의 경우, 트랜잭션은 브리지를 통해 그레인 저장소로 전환을 시도합니다. 그레인 스토리지로의 브리지를 통한 트랜잭션 상태에 대한 액세스는 비효율적이며 향후 지원되지 않을 수도 있습니다. 따라서 개발 목적으로만 이 방법을 사용하는 것이 좋습니다.

곡물 인터페이스

트랜잭션을 지원하려면, 해당 그레인의 인터페이스에서 TransactionAttribute를 사용하여 트랜잭션 메서드를 트랜잭션의 일부로 표시해야 합니다. 특성은 다음 TransactionOption 값에 자세히 설명된 대로 트랜잭션 환경에서 곡물 호출이 작동하는 방식을 나타내야 합니다.

  • TransactionOption.Create: 호출은 트랜잭션이며 기존 트랜잭션 컨텍스트 내에서 호출되더라도 항상 새 트랜잭션 컨텍스트를 만듭니다(새 트랜잭션을 시작함).
  • TransactionOption.Join: 호출은 트랜잭션이지만 기존 트랜잭션의 컨텍스트 내에서만 호출할 수 있습니다.
  • TransactionOption.CreateOrJoin: 호출은 거래적입니다. 트랜잭션의 컨텍스트 내에서 호출되는 경우 해당 컨텍스트를 사용하고, 그렇지 않으면 새 컨텍스트를 만듭니다.
  • TransactionOption.Suppress: 호출 자체는 트랜잭션과 관련이 없지만, 트랜잭션 내에서 호출할 수 있습니다. 트랜잭션의 컨텍스트 내에서 호출되는 경우 컨텍스트는 호출에 전달되지 않습니다.
  • TransactionOption.Supported: 호출은 트랜잭션을 직접 수행하지 않지만 트랜잭션을 지원합니다. 트랜잭션의 컨텍스트 내에서 호출되는 경우 컨텍스트가 호출에 전달됩니다.
  • TransactionOption.NotAllowed: 이 호출은 트랜잭션적이지 않으며 트랜잭션 내에서 호출할 수 없습니다. 트랜잭션의 컨텍스트 내에서 호출되면 NotSupportedException가 던져집니다.

호출을 TransactionOption.Create으로 표시할 수 있습니다. 즉, 호출이 항상 트랜잭션을 시작합니다. 예를 들어 Transfer ATM 그레인 아래의 작업은 항상 두 개의 계정을 참조하여 새로운 트랜잭션을 시작합니다.

namespace TransactionalExample.Abstractions;

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

트랜잭션 작업 WithdrawDeposit이 계정 grain에서 TransactionOption.Join으로 표시됩니다. 이는 기존 트랜잭션의 컨텍스트 내에서만 호출할 수 있음을 나타내며, IAtmGrain.Transfer 중에 호출되는 경우가 이에 해당됩니다. 호출이 GetBalance로 표시되어 있으므로 기존 트랜잭션 내에서(예를 들어, CreateOrJoin를 통해) 또는 자체적으로 호출할 수 있습니다.

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

중요한 고려 사항

이러한 호출에는 호출 전에 적절한 설정이 필요하므로 트랜잭션으로 표시 OnActivateAsync 할 수 없습니다. 곡물 애플리케이션 API에 대해서만 존재합니다. 즉, 이러한 메서드를 통해 트랜잭션 상태를 읽으려고 시도하면 실행 시간 중에 예외가 발생합니다.

곡물 구현

그래인 구현은 ITransactionalState<TState> 패싯을 사용하여 ACID 트랜잭션을 통해 그래인 상태를 관리해야 합니다.

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

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

트랜잭션 상태 패싯에 전달된 동기 함수를 통해 지속형 상태에 대한 모든 읽기 또는 쓰기 액세스를 수행합니다. 이렇게 하면 트랜잭션 시스템에서 이러한 작업을 트랜잭션 방식으로 수행하거나 취소할 수 있습니다. 그레인 내에서 트랜잭션 상태를 사용하려면 지속되도록 직렬화할 수 있는 상태 클래스를 정의하고, 를 사용하여 TransactionalStateAttribute그레인의 생성자에서 트랜잭션 상태를 선언합니다. 이 특성은 상태 이름과 필요에 따라 사용할 트랜잭션 상태 스토리지를 선언합니다. 자세한 내용은 설치 프로그램을 참조하세요.

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

예를 들어 Balance 상태 개체는 다음과 같이 정의됩니다.

namespace TransactionalExample.Abstractions;

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

이전 상태 개체:

  • GenerateSerializerAttribute를 사용하여 serializer를 생성하라는 지시를 코드 생성기에게 하는 데코레이션이 이루어집니다.
  • Value 속성으로 데코레이팅되어 멤버를 IdAttribute으로 고유하게 식별합니다.

Balance 상태 개체는 다음과 같이 AccountGrain 구현에 사용됩니다.

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

중요합니다

트랜잭션 컨텍스트가 그레인 호출에 올바르게 전달되도록 하기 위해 그레인을 ReentrantAttribute으로 표시해야 합니다.

앞의 예제 TransactionalStateAttribute 에서 생성자 매개 변수는 balance 명명 "balance"된 트랜잭션 상태와 연결되어야 한다고 선언합니다. 이 선언을 통해 Orleans는 이름이 ITransactionalState<TState>인 트랜잭션 상태 스토리지에서 로드된 상태를 사용하여 "TransactionStore" 인스턴스를 삽입합니다. PerformUpdate을 통해 상태를 수정하거나 PerformRead을 통해 읽어 볼 수 있습니다. 트랜잭션 인프라는 트랜잭션의 일부로 수행된 모든 변경 내용(클러스터에 Orleans 분산된 여러 곡물 중에서도)이 모두 커밋되거나 트랜잭션을 만든 곡물 호출이 완료될 때 모두 실행 취소되도록 합니다(IAtmGrain.Transfer 이전 예제에서).

클라이언트에서 트랜잭션 메서드 호출

트랜잭션 그레인 메서드를 호출하는 권장 방법은 ITransactionClient를 사용하는 것입니다. 클라이언트를 Orleans 구성할 때, ITransactionClient은 종속성 주입 서비스 공급자에 Orleans가 자동으로 등록됩니다. 트랜잭션 컨텍스트를 생성하고 해당 컨텍스트 내에서 트랜잭션 그레인 메서드를 호출하는 데 ITransactionClient를 사용합니다. 다음 예제에서는 ITransactionClient를 사용하여 트랜잭션 곡물 메서드를 호출하는 방법을 보여 줍니다.

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

이전 클라이언트 코드에서 다음을 수행합니다.

  • IHostBuilderUseOrleansClient로 구성됩니다.
    • localhost IClientBuilder 클러스터링 및 트랜잭션을 사용합니다.
  • IClusterClientITransactionClient 인터페이스는 서비스 공급자에서 검색됩니다.
  • fromto 변수에 IAccountGrain 참조가 할당됩니다.
  • ITransactionClient은 트랜잭션을 생성하는 데 사용되며 다음을 호출합니다.
    • Withdraw 계정 속성 참조에서 from
    • Deposit 계정 속성 참조에서 to

예외가 transactionDelegate에서 throw되거나 모순 transactionOption이 지정된 경우를 제외하고 트랜잭션은 항상 커밋됩니다. 트랜잭션 그레인 메서드를 호출하는 권장 방법은 ITransactionClient을 사용하는 것이지만, 다른 그레인에서 직접 호출할 수도 있습니다.

다른 그레인에서 트랜잭션 메서드 호출

다른 곡물 메서드와 마찬가지로 곡물 인터페이스에서 트랜잭션 메서드를 호출합니다. ITransactionClient를 사용하는 대신, 아래 구현에서는 AtmGrain 인터페이스에서 Transfer 메서드(트랜잭셔널한)를 호출합니다.

AtmGrain의 구현에서는 두 개의 참조된 계정 요소를 해소하고 WithdrawDeposit에 적절한 호출을 수행합니다.

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

클라이언트 앱 코드는 다음과 같이 트랜잭션으로 호출 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();

이전 호출 IAtmGrain 에서는 한 계정에서 다른 계정으로 100 단위의 통화를 전송하는 데 사용됩니다. 전송이 완료되면 두 계정이 모두 쿼리되어 현재 잔액을 가져옵니다. 통화 전송과 두 계정 쿼리는 ACID 트랜잭션으로 수행됩니다.

앞의 예제와 같이 트랜잭션은 다른 그레인 호출과 같이 Task 내에서 값을 반환할 수 있습니다. 그러나 호출 실패 시 애플리케이션 예외를 발생시키지 않고 대신 OrleansTransactionException 또는 TimeoutException을(를) 발생시킵니다. 애플리케이션이 트랜잭션 중에 예외를 throw하고 해당 예외로 인해 트랜잭션이 실패하는 경우(다른 시스템 오류로 인해 실패하지 않음) 애플리케이션 예외는 내부 예외 OrleansTransactionException가 됩니다.

형식 OrleansTransactionAbortedException 의 트랜잭션 예외가 throw되면 트랜잭션이 실패하여 다시 시도될 수 있습니다. throw된 다른 예외는 알 수 없는 상태로 종료된 트랜잭션을 나타냅니다. 트랜잭션은 분산 작업이므로 알 수 없는 상태의 트랜잭션이 성공하거나 실패했거나 진행 중일 수 있습니다. 이러한 이유로 상태를 확인하거나 작업을 다시 시도하기 전에 호출 시간 제한 기간(SiloMessagingOptions.SystemResponseTimeout)을 통과하여 연속 중단을 방지하는 것이 좋습니다.