Transakcje w warstwie Orleans
Orleans obsługuje rozproszone transakcje ACID względem trwałego stanu ziarna. Transakcje są implementowane przy użyciu platformy Microsoft.Orleans. Pakiet NuGet transakcji . Kod źródłowy przykładowej aplikacji w tym artykule składa się z czterech projektów:
- Abstrakcje: biblioteka klas zawierająca interfejsy ziarna i udostępnione klasy.
- Ziarna: biblioteka klas zawierająca implementacje ziarna.
- Serwer: aplikacja konsolowa, która korzysta z bibliotek klas abstrakcji i ziarna i działa jako Orleans silos.
- Klient: aplikacja konsolowa, która korzysta z biblioteki klas abstrakcji, która reprezentuje Orleans klienta.
Ustawienia
Orleans transakcje są opt-in. Zarówno silos, jak i klient muszą być skonfigurowane do używania transakcji. Jeśli nie są skonfigurowane, wszystkie wywołania metod transakcyjnych w implementacji ziarna otrzymają .OrleansTransactionsDisabledException Aby włączyć transakcje na silosie, wywołaj SiloBuilderExtensions.UseTransactions konstruktora hosta silosu:
var builder = Host.CreateDefaultBuilder(args)
UseOrleans((context, siloBuilder) =>
{
siloBuilder.UseTransactions();
});
Podobnie, aby włączyć transakcje na kliencie, wywołaj ClientBuilderExtensions.UseTransactions konstruktor hosta klienta:
var builder = Host.CreateDefaultBuilder(args)
UseOrleansClient((context, clientBuilder) =>
{
clientBuilder.UseTransactions();
});
Magazyn stanu transakcyjnego
Aby korzystać z transakcji, należy skonfigurować magazyn danych. Aby obsługiwać różne magazyny danych z transakcjami, używana jest abstrakcja ITransactionalStateStorage<TState> magazynu. Ta abstrakcja jest specyficzna dla potrzeb transakcji, w przeciwieństwie do ogólnego magazynu ziarna (IGrainStorage). Aby użyć magazynu specyficznego dla transakcji, skonfiguruj silos przy użyciu dowolnej implementacji ITransactionalStateStorage
, takiej jak Azure (AddAzureTableTransactionalStateStorage).
Rozważmy na przykład następującą konfigurację konstruktora hostów:
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();
W celach programistycznych, jeśli magazyn specyficzny dla transakcji nie jest dostępny dla potrzebnego magazynu danych, możesz zamiast tego użyć IGrainStorage
implementacji. W przypadku dowolnego stanu transakcyjnego, który nie ma skonfigurowanego magazynu, transakcje będą próbować przejść w tryb failover do magazynu ziarna przy użyciu mostka. Uzyskiwanie dostępu do stanu transakcyjnego za pośrednictwem mostka do magazynowania ziarna jest mniej wydajne i może nie być obsługiwane w przyszłości. W związku z tym zaleceniem jest użycie go tylko do celów programistycznych.
Interfejsy ziarna
Aby ziarno obsługiwało transakcje, metody transakcyjne w interfejsie ziarna muszą być oznaczone jako część transakcji przy użyciu .TransactionAttribute Atrybut musi wskazywać, jak działa wywołanie ziarna w środowisku transakcyjnym zgodnie z poniższymi TransactionOption wartościami:
- TransactionOption.Create: Wywołanie jest transakcyjne i zawsze tworzy nowy kontekst transakcji (uruchamia nową transakcję), nawet jeśli jest wywoływane w istniejącym kontekście transakcji.
- TransactionOption.Join: Wywołanie jest transakcyjne, ale może być wywoływane tylko w kontekście istniejącej transakcji.
- TransactionOption.CreateOrJoin: Wywołanie jest transakcyjne. Jeśli zostanie wywołana w kontekście transakcji, będzie używać tego kontekstu, w przeciwnym razie utworzy nowy kontekst.
- TransactionOption.Suppress: Wywołanie nie jest transakcyjne, ale może być wywoływane z poziomu transakcji. Jeśli zostanie wywołana w kontekście transakcji, kontekst nie zostanie przekazany do wywołania.
- TransactionOption.Supported: Wywołanie nie jest transakcyjne, ale obsługuje transakcje. Jeśli zostanie wywołana w kontekście transakcji, kontekst zostanie przekazany do wywołania.
- TransactionOption.NotAllowed: Wywołanie nie jest transakcyjne i nie może być wywoływane z poziomu transakcji. Jeśli wywołana w kontekście transakcji, zwróci wartość NotSupportedException.
Wywołania mogą być oznaczone jako TransactionOption.Create
, co oznacza, że wywołanie zawsze rozpocznie transakcję. Na przykład Transfer
operacja w poniższym ziarnie atm zawsze rozpoczyna nową transakcję, która obejmuje dwa przywołyzowane konta.
namespace TransactionalExample.Abstractions;
public interface IAtmGrain : IGrainWithIntegerKey
{
[Transaction(TransactionOption.Create)]
Task Transfer(string fromId, string toId, decimal amountToTransfer);
}
Operacje Withdraw
transakcyjne i Deposit
ziarna konta są oznaczone jako TransactionOption.Join
, co wskazuje, że mogą być wywoływane tylko w kontekście istniejącej transakcji, co byłoby przypadkiem wywołania podczas .IAtmGrain.Transfer
Wywołanie GetBalance
jest oznaczone CreateOrJoin
, aby można było wywołać je z poziomu istniejącej transakcji, na przykład za pośrednictwem IAtmGrain.Transfer
metody lub samodzielnie.
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();
}
Ważne uwagi
OnActivateAsync
Nie można oznaczyć elementu jako transakcyjnego, ponieważ takie wywołanie wymaga odpowiedniej konfiguracji przed wywołaniem. Istnieje tylko dla interfejsu API aplikacji ziarna. Oznacza to, że próba odczytania stanu transakcyjnego w ramach tych metod zgłosi wyjątek w środowisku uruchomieniowym.
Implementacje ziarna
Implementacja ziarna musi używać ITransactionalState<TState> aspektu do zarządzania stanem ziarna za pośrednictwem transakcji 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);
}
Cały dostęp do odczytu lub zapisu do stanu utrwalonego musi być wykonywany za pośrednictwem funkcji synchronicznych przekazywanych do aspektu stanu transakcyjnego. Dzięki temu system transakcji może wykonywać lub anulować te operacje transakcyjnie. Aby użyć stanu transakcyjnego w ziarnie, zdefiniuj klasę stanu z możliwością serializacji i zadeklaruj stan transakcyjny w konstruktorze ziarna za pomocą klasy TransactionalStateAttribute. Ten ostatni deklaruje nazwę stanu i, opcjonalnie, który transakcyjny magazyn stanu ma być używany. Aby uzyskać więcej informacji, zobacz Setup (Konfiguracja).
[AttributeUsage(AttributeTargets.Parameter)]
public class TransactionalStateAttribute : Attribute
{
public TransactionalStateAttribute(string stateName, string storageName = null)
{
// ...
}
}
Na przykład Balance
obiekt stanu jest definiowany w następujący sposób:
namespace TransactionalExample.Abstractions;
[GenerateSerializer]
public record class Balance
{
[Id(0)]
public decimal Value { get; set; } = 1_000;
}
Poprzedni obiekt stanu:
- Element jest ozdobiony elementem GenerateSerializerAttribute w celu poinstruowania generatora kodu o wygenerowaniu Orleans serializatora.
Value
Ma właściwość, która jest ozdobiona elementemIdAttribute
, aby jednoznacznie zidentyfikować element członkowski.
Obiekt Balance
stanu jest następnie używany w implementacji AccountGrain
w następujący sposób:
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);
}
Ważne
Ziarno transakcyjne musi być oznaczone znakiem , ReentrantAttribute aby upewnić się, że kontekst transakcji jest poprawnie przekazywany do wywołania ziarna.
W poprzednim przykładzie parametr jest używany do deklarowania, TransactionalStateAttribute że balance
parametr konstruktora powinien być skojarzony ze stanem transakcyjnym o nazwie "balance"
. Ta deklaracja Orleans spowoduje wstrzyknięcie ITransactionalState<TState> wystąpienia ze stanem załadowanym z magazynu stanu transakcyjnego o nazwie "TransactionStore"
. Stan można modyfikować za pomocą polecenia lub odczytywać za pomocą PerformUpdate
polecenia PerformRead
. Infrastruktura transakcji zapewni, że wszelkie takie zmiany wykonywane w ramach transakcji, nawet między wieloma ziarnami rozproszonymi w Orleans klastrze, zostaną zatwierdzone lub wszystkie zostaną cofnięte po zakończeniu wywołania ziarna, które utworzyły transakcję (IAtmGrain.Transfer
w poprzednim przykładzie).
Wywoływanie metod transakcji od klienta
Zalecanym sposobem wywoływania metody ziarna transakcji jest użycie metody ITransactionClient
. Element ITransactionClient
jest automatycznie rejestrowany u dostawcy usługi wstrzykiwania zależności po skonfigurowaniu Orleans klienta. Służy ITransactionClient
do tworzenia kontekstu transakcji i wywoływania metod ziarna transakcyjnego w tym kontekście. W poniższym przykładzie pokazano, jak używać ITransactionClient
metody do wywoływania metod ziarna transakcyjnego.
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));
}
W poprzednim kodzie klienta:
- Element
IHostBuilder
jest skonfigurowany za pomocą poleceniaUseOrleansClient
.- Funkcja
IClientBuilder
używa klastrowania localhost i transakcji.
- Funkcja
- Interfejsy
IClusterClient
iITransactionClient
są pobierane z dostawcy usług. - Zmienne
from
ito
są przypisane do ichIAccountGrain
odwołań. - Element
ITransactionClient
służy do tworzenia transakcji, wywołując:Withdraw
w odwołaniufrom
do ziarna konta.Deposit
w odwołaniuto
do ziarna konta.
Transakcje są zawsze zatwierdzane, chyba że istnieje wyjątek zgłaszany w transactionDelegate
określonym elem lub sprzeczne transactionOption
. Chociaż zalecanym sposobem wywoływania metod ziarna transakcyjnego jest użycie ITransactionClient
metody , można również wywołać metody ziarna transakcyjnego bezpośrednio z innego ziarna.
Wywoływanie metod transakcji z innego ziarna
Metody transakcyjne w interfejsie ziarna są wywoływane jak każda inna metoda ziarna. Jako alternatywne podejście przy użyciu ITransactionClient
AtmGrain
metody implementacja poniżej wywołuje Transfer
metodę (która jest transakcyjna) w interfejsieIAccountGrain
.
Rozważ implementację AtmGrain
, która rozpoznaje dwa przywołyte ziarna kont i wykonuje odpowiednie wywołania do Withdraw
i 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));
}
Kod aplikacji klienckiej może wywoływać AtmGrain.Transfer
w sposób transakcyjny w następujący sposób:
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();
W poprzednich wywołaniach IAtmGrain
element jest używany do transferu 100 jednostek waluty z jednego konta do drugiego. Po zakończeniu transferu oba konta są odpytywane, aby uzyskać bieżące saldo. Transfer waluty, a także zapytania dotyczące konta, są wykonywane jako transakcje ACID.
Jak pokazano w poprzednim przykładzie, transakcje mogą zwracać wartości w obiekcie Task
, podobnie jak inne wywołania ziarna. Jednak po niepowodzeniu wywołania nie będą zgłaszać wyjątków aplikacji, ale raczej błędu OrleansTransactionException lub TimeoutException. Jeśli aplikacja zgłasza wyjątek podczas transakcji i że wyjątek powoduje niepowodzenie transakcji (w przeciwieństwie do niepowodzenia z powodu innych awarii systemu), wyjątek aplikacji będzie wyjątkiem OrleansTransactionException
wewnętrznym .
Jeśli zgłaszany jest wyjątek transakcji typu OrleansTransactionAbortedException, transakcja nie powiodła się i może zostać ponowiona. Każdy inny zgłoszony wyjątek wskazuje, że transakcja została zakończona z nieznanym stanem. Ponieważ transakcje są operacjami rozproszonymi, transakcja w nieznanym stanie mogła zakończyć się powodzeniem, niepowodzeniem lub nadal trwać. Z tego powodu zaleca się zezwolenie na przekazywanie limitu czasu wywołania (SiloMessagingOptions.SystemResponseTimeout), aby uniknąć przerywania kaskadowego przed zweryfikowaniem stanu lub ponowieniu próby wykonania operacji.