Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Orleans dá suporte a transações ACID distribuídas em relação ao estado de grão persistente. As transações são implementadas usando o pacote NuGet Microsoft.Orleans.Transactions. O código-fonte do aplicativo de exemplo neste artigo consiste em quatro projetos:
- Abstrações: uma biblioteca de classes que contém as interfaces de grãos e classes compartilhadas.
- Grãos: uma biblioteca de classes que contém as implementações de Grãos.
- Servidor: um aplicativo de console que consome as abstrações e granula as bibliotecas de classes e atua como o Orleans silo.
- Cliente: um aplicativo de console que consome a biblioteca de classes de abstrações que representa o Orleans cliente.
Configuração
Orleans as transações são opcionais. O silo e o cliente devem ser configurados para usar transações. Se eles não estiverem configurados, todas as chamadas para métodos transacionais em uma implementação de grão receberão um OrleansTransactionsDisabledException. Para habilitar transações em um silo, chame SiloBuilderExtensions.UseTransactions o construtor de host do silo:
var builder = Host.CreateDefaultBuilder(args)
.UseOrleans((context, siloBuilder) =>
{
siloBuilder.UseTransactions();
});
Da mesma forma, para habilitar transações no cliente, chame ClientBuilderExtensions.UseTransactions o construtor de host do cliente:
var builder = Host.CreateDefaultBuilder(args)
.UseOrleansClient((context, clientBuilder) =>
{
clientBuilder.UseTransactions();
});
Armazenamento de estado transacional
Para usar transações, você precisa configurar um armazenamento de dados. Para dar suporte a vários armazenamentos de dados com transações, Orleans usa a abstração ITransactionalStateStorage<TState>de armazenamento. Essa abstração é específica às necessidades das transações, ao contrário do armazenamento genérico de grãos (IGrainStorage). Para usar o armazenamento específico da transação, configure o silo usando qualquer implementação de ITransactionalStateStorage, como o Azure (AddAzureTableTransactionalStateStorage).
Por exemplo, considere a seguinte configuração do construtor de host:
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();
Para fins de desenvolvimento, se não houver armazenamento específico de transação disponível para o datastore necessário, você poderá usar uma implementação IGrainStorage. Para qualquer estado transacional sem um repositório configurado, as transações tentam realizar o failover para o armazenamento de grãos utilizando uma ponte. Acessar o estado transacional por meio de uma ponte para o armazenamento de grãos é menos eficiente e pode não ter suporte no futuro. Portanto, recomendamos usar essa abordagem apenas para fins de desenvolvimento.
Interfaces de grãos
Para que um grão dê suporte a transações, você deve marcar métodos transacionais em sua interface de grãos como parte de uma transação usando o TransactionAttribute. O atributo precisa indicar como a chamada de grão se comporta em um ambiente transacional, conforme detalhado pelos seguintes TransactionOption valores:
- TransactionOption.Create: a chamada é transacional e sempre criará um novo contexto de transação (inicia uma nova transação), mesmo se chamada dentro de um contexto de transação existente.
- TransactionOption.Join: a chamada é transacional, mas só pode ser chamada dentro do contexto de uma transação existente.
- TransactionOption.CreateOrJoin: a chamada é transacional. Se chamado dentro do contexto de uma transação, ele usará esse contexto, caso contrário, criará um novo contexto.
- TransactionOption.Suppress: a chamada não é transacional, mas pode ser chamada de dentro de uma transação. Se for chamado dentro do contexto de uma transação, o contexto não será passado para a chamada.
- TransactionOption.Supported: a chamada não é transacional, mas dá suporte a transações. Se chamado dentro do contexto de uma transação, o contexto será passado para a chamada.
- TransactionOption.NotAllowed: a chamada não é transacional e não pode ser chamada de dentro de uma transação. Se chamado dentro do contexto de uma transação, ele gerará o NotSupportedException.
Você pode marcar chamadas como TransactionOption.Create, o que significa que a chamada sempre inicia sua transação. Por exemplo, a operação Transfer no grão de ATM mencionado sempre inicia uma nova transação envolvendo as duas contas referenciadas.
namespace TransactionalExample.Abstractions;
public interface IAtmGrain : IGrainWithIntegerKey
{
[Transaction(TransactionOption.Create)]
Task Transfer(string fromId, string toId, decimal amountToTransfer);
}
As operações transacionais Withdraw e Deposit no grão da conta são marcadas TransactionOption.Join. Isso indica que eles só podem ser chamados dentro do contexto de uma transação existente, o que seria o caso se chamado durante IAtmGrain.Transfer. A GetBalance chamada está marcada CreateOrJoin, para que você possa chamá-la de dentro de uma transação existente (como via IAtmGrain.Transfer) ou de forma independente.
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();
}
Considerações importantes
Você não pode marcar OnActivateAsync como transacional porque essa chamada requer a configuração adequada antes da chamada. Isso existe apenas para a API de aplicação de grãos. Isso significa que tentar ler o estado transacional como parte desses métodos gera uma exceção no runtime.
Implementações de grãos
Uma implementação de grain precisa usar uma ITransactionalState<TState> faceta para gerenciar o estado de grain por meio de transações 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);
}
Execute todo o acesso de leitura ou gravação ao estado persistente por meio de funções síncronas passadas para a faceta de estado transacional. Isso permite que o sistema de transações execute ou cancele essas operações transacionalmente. Para usar o estado transacional em um grão, defina uma classe de estado serializável a ser mantida e declare o estado transacional no construtor do grão usando um TransactionalStateAttribute. Esse atributo declara o nome do estado e, opcionalmente, qual armazenamento de estado transacional usar. Para obter mais informações, consulte Instalação.
[AttributeUsage(AttributeTargets.Parameter)]
public class TransactionalStateAttribute : Attribute
{
public TransactionalStateAttribute(string stateName, string storageName = null)
{
// ...
}
}
Por exemplo, o objeto de Balance estado é definido da seguinte maneira:
namespace TransactionalExample.Abstractions;
[GenerateSerializer]
public record class Balance
{
[Id(0)]
public decimal Value { get; set; } = 1_000;
}
O objeto de estado anterior:
- É decorado com GenerateSerializerAttribute para instruir o gerador de código Orleans a gerar um serializador.
- Tem uma
Valuepropriedade decorada com oIdAttributepara identificar exclusivamente o membro.
O objeto de estado Balance é então usado na implementação AccountGrain da seguinte maneira:
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);
}
Importante
Um grão transacional deve ser marcado com o ReentrantAttribute para garantir que o contexto da transação seja passado corretamente para a chamada de grãos.
No exemplo anterior, o TransactionalStateAttribute declara que o balance parâmetro do construtor deve ser associado a um estado transacional chamado "balance". Com essa declaração, Orleans injeta uma ITransactionalState<TState> instância com o estado carregado do armazenamento de estado transacional chamado "TransactionStore". Você pode modificar o estado por meio PerformUpdate ou lê-lo por meio de PerformRead. A infraestrutura de transação garante que todas essas alterações executadas como parte de uma transação (mesmo entre vários grãos distribuídos em um Orleans cluster) sejam todas confirmadas ou todas desfeitas após a conclusão da chamada de grão que criou a transação (IAtmGrain.Transfer no exemplo anterior).
Chamar métodos de transação de um cliente
A maneira recomendada de chamar um método de granulação transacional é usar o ITransactionClient.
Orleans é registrado automaticamente com o serviço de injeção de dependência ITransactionClient quando o cliente Orleans é configurado. Use ITransactionClient para criar um contexto de transação e chamar métodos de granulação transacional dentro desse contexto. O exemplo a seguir mostra como usar ITransactionClient para chamar métodos de granulação transacional.
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));
}
No código do cliente anterior:
-
IHostBuilderé configurado comUseOrleansClient.- O
IClientBuilderusa clustering em localhost e transações.
- O
- As interfaces
IClusterClienteITransactionClientsão recuperadas do provedor de serviços. - As
fromvariáveis e astovariáveis recebem suasIAccountGrainreferências. - O
ITransactionClienté usado para criar uma transação, chamando:-
Withdrawna conta de referência de grãosfrom. -
Depositna conta de referência de grãosto.
-
As transações são sempre completadas, a menos que uma exceção seja gerada no transactionDelegate ou uma oposição transactionOption seja especificada. Embora o uso ITransactionClient seja a maneira recomendada de chamar métodos de grão transacional, você também pode chamá-los diretamente de outro grão.
Chamar métodos de transação de outro grão
Chame métodos transacionais em uma interface de grão como qualquer outro método de grão. Como alternativa ao uso de ITransactionClient, a implementação AtmGrain abaixo chama o método Transfer (que é transacional) na interface IAccountGrain.
Considere a AtmGrain implementação, que resolve os dois elementos de conta referenciados e faz as chamadas apropriadas para Withdraw e 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));
}
O código do aplicativo cliente pode chamar AtmGrain.Transfer transacionalmente da seguinte maneira:
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();
Nas chamadas anteriores, um IAtmGrain é usado para transferir 100 unidades de moeda de uma conta para outra. Após a conclusão da transferência, ambas as contas são consultadas para obter o saldo atual. A transferência de moeda, bem como as consultas de conta, são executadas como transações ACID.
Como mostrado no exemplo anterior, as transações podem retornar valores dentro de um objeto Task, como em outras chamadas de grain. No entanto, em caso de falha na chamada, eles não geram exceções de aplicativo, mas sim um OrleansTransactionException ou TimeoutException. Se o aplicativo gerar uma exceção durante a transação e essa exceção fizer com que a transação falhe (em vez de falhar devido a outras falhas do sistema), a exceção do aplicativo se tornará a exceção interna do OrleansTransactionException.
Se uma exceção de transação do tipo OrleansTransactionAbortedException for gerada, a transação falhará e poderá ser repetida. Qualquer outra exceção gerada indica que a transação foi encerrada com um estado desconhecido. Como as transações são operações distribuídas, uma transação em um estado desconhecido pode ter sido bem-sucedida, com falha ou ainda em andamento. Por esse motivo, é aconselhável permitir que um período de tempo limite de chamada (SiloMessagingOptions.SystemResponseTimeout) passe antes de verificar o estado ou tentar novamente a operação para evitar anulações em cascata.