Nuta
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować się zalogować lub zmienić katalog.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Orleans obsługuje rozproszone transakcje ACID względem trwałego stanu ziarna. Transakcje są implementowane przy użyciu pakietu NuGet Microsoft.Orleans.Transactions. Kod źródłowy przykładowej aplikacji w tym artykule składa się z czterech projektów:
- Abstrakcje: biblioteka klas zawierająca interfejsy "grain" i współdzielone 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.
Konfiguracja
Orleans transakcje wymagają zgody. 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ą element 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 na konstruktorze 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 w ramach transakcji, Orleans używa abstrakcji magazynu ITransactionalStateStorage<TState>. 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 bez skonfigurowanego magazynu, transakcje próbują przełączyć się na magazyn ziarna przy użyciu pomostu. Uzyskiwanie dostępu do stanu transakcyjnego za pośrednictwem mostka do magazynu ziarna jest mniej wydajne i może nie być obsługiwane w przyszłości. W związku z tym zalecamy użycie tego podejścia tylko do celów programistycznych.
Interfejsy ziarna
Aby ziarno obsługiwało transakcje, należy oznaczyć metody transakcyjne w interfejsie ziarna jako część transakcji, używając TransactionAttribute. Atrybut musi wskazywać, jak wywołanie grain zachowuje się w środowisku transakcyjnym, zgodnie z poniższymi wartościami TransactionOption:
- 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ć wykonywane 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 ramach kontekstu transakcji, kontekst ten zostanie przekazany do wywołania.
- TransactionOption.NotAllowed: Wywołanie nie jest transakcyjne i nie może być wywoływane w ramach transakcji. Jeśli wywołana w kontekście transakcji, zwróci wartość NotSupportedException.
Wywołania można oznaczyć jako TransactionOption.Create, co oznacza, że wywołanie zawsze uruchamia jego transakcję. Na przykład operacja Transfer w poniższym ziarenku ATM zawsze rozpoczyna nową transakcję obejmującą dwa przywołane 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. Oznacza to, że mogą być wywoływane tylko w kontekście istniejącej transakcji, co ma miejsce, gdy są wywoływane podczas IAtmGrain.Transfer. Wywołanie GetBalance jest oznaczone jako CreateOrJoin, więc można je wywołać z istniejącej transakcji (jak poprzez IAtmGrain.Transfer) 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
Nie można oznaczyć OnActivateAsync jako transakcyjnego, ponieważ takie wywołanie wymaga prawidłowej konfiguracji przed wywołaniem. Istnieje tylko dla interfejsu API aplikacji ziarna. Oznacza to, że próba odczytu stanu transakcyjnego w ramach tych metod zgłasza wyjątek w środowisku uruchomieniowym.
Implementacje modułu Grain
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);
}
Wykonaj wszystkie operacje odczytu lub zapisu w stanie utrwalonym za pomocą funkcji synchronicznych przekazywanych do modułu stanu transakcyjnego. Dzięki temu system transakcji może wykonywać lub anulować te operacje transakcyjnie. Aby użyć stanu transakcyjnego w ziarnie, zdefiniuj serializowalną klasę stanu i zadeklaruj stan transakcyjny w konstruktorze ziarna przy użyciu TransactionalStateAttribute. Ten atrybut deklaruje nazwę stanu i, opcjonalnie, który transakcyjny magazyn stanów ma zostać użyty. 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, aby generator kodu Orleans wygenerował serializator.
- Właściwość
Value, która jest ozdobiona elementemIdAttribute, aby jednoznacznie zidentyfikować element.
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 przy użyciu ReentrantAttribute, aby upewnić się, że kontekst transakcji jest poprawnie przekazywany podczas wywoływania ziarna.
W poprzednim przykładzie zadeklarowano TransactionalStateAttribute , że balance parametr konstruktora powinien być skojarzony ze stanem transakcyjnym o nazwie "balance". Deklaracja ta powoduje, że Orleans wstrzykuje instancję ITransactionalState<TState> z załadowanym stanem z magazynu stanu transakcyjnego o nazwie "TransactionStore". Stan można zmodyfikować za pomocą PerformUpdate lub odczytać za pomocą PerformRead. Infrastruktura transakcji zapewnia, że wszelkie takie zmiany wykonywane w ramach transakcji (nawet między wieloma ziarnami rozproszonymi w Orleans klastrze) są albo wszystkie zatwierdzane, albo wszystkie cofane po zakończeniu wywołania ziarna, które utworzyło transakcję (IAtmGrain.Transfer w poprzednim przykładzie).
Wywoływanie metod transakcji od klienta
Zalecanym sposobem wywoływania metody ziarna transakcyjnego jest użycie ITransactionClient.
Orleans program automatycznie rejestruje się u dostawcy ITransactionClient usługi wstrzykiwania zależności podczas konfigurowania Orleans klienta. Użyj ITransactionClient do stworzenia kontekstu transakcji i wywoływania metod transakcyjnych ziaren w tym kontekście. W poniższym przykładzie pokazano, jak użyć ITransactionClient do wywoływania metod transakcyjnego ziarna.
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
IHostBuilderjest skonfigurowany za pomocąUseOrleansClient.- Funkcja
IClientBuilderużywa klastrowania localhost i transakcji.
- Funkcja
- Interfejsy
IClusterClientiITransactionClientsą pobierane z dostawcy usług. - Zmienne
fromitosą przypisywane ich odwołaniomIAccountGrain. - Element
ITransactionClientsłuży do tworzenia transakcji, wywołując:-
Withdraww odniesieniu do szczegółówfromkonta. -
Depositw odniesieniu do szczegółówtokonta.
-
Transakcje są zawsze zatwierdzane, chyba że w transactionDelegate zostanie zgłoszony wyjątek lub określony zostanie konfliktowy transactionOption. Chociaż użycie ITransactionClient jest zalecanym sposobem wywoływania metod ziarna transakcyjnego, można także wywołać te metody bezpośrednio z innego ziarna.
Wywoływanie metod transakcji z innego ziarna
Wywołaj metody transakcyjne w interfejsie ziarna, podobnie jak każda inna metoda ziarna. Zamiast używania ITransactionClientAtmGrain 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 transakcyjnie 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 jest używany do transferu 100 jednostek waluty z jednego konta na drugie. 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 zgłaszają wyjątków aplikacji, ale raczej błędu OrleansTransactionException lub TimeoutException. Jeśli aplikacja zgłasza wyjątek podczas transakcji, a wyjątek powoduje niepowodzenie transakcji (w przeciwieństwie do niepowodzenia z powodu innych awarii systemu), wyjątek aplikacji staje się wyjątkiem OrleansTransactionExceptionwewnę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 oznacza, że transakcja zakończyła się w stanie nieznanym. Ponieważ transakcje są operacjami rozproszonymi, transakcja w nieznanym stanie mogła zakończyć się powodzeniem, niepowodzeniem lub nadal trwać. Z tego powodu zaleca się pozwolenie na upłynięcie limitu czasu połączenia (SiloMessagingOptions.SystemResponseTimeout) przed zweryfikowaniem stanu lub ponownym podjęciem próby wykonania operacji, aby uniknąć kaskadowych przerwań.