Orleans 項交易

Orleans 支援永續性粒紋狀態的分散式 ACID 交易。 交易可使用 Microsoft.Orleans.Transactions NuGet 套件來實作。 本文中的範例應用程式的原始程式碼由四個專案組成:

  • 抽象概念:包含粒紋介面和共用類別的類別庫。
  • 粒紋:包含粒紋實作的類別庫。
  • 伺服器:使用抽象和粒紋類別庫,並且作為 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();
    });

交易狀態儲存體

若要使用交易,您必須設定資料存放區。 為了支援交易的各種資料存放區,我們使用儲存體抽象概念 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,這表示呼叫一律會啟動其交易。 例如,ATM 粒紋中的下列 Transfer 作業一律會啟動涉及兩個參考帳戶的新交易。

namespace TransactionalExample.Abstractions;

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

帳戶粒紋的交易作業 WithdrawDeposit 會標示為 TransactionOption.Join,指出只能在現有交易的內容中加以呼叫,如果在 IAtmGrain.Transfer 期間呼叫,就會出現這種情況。 呼叫 GetBalance 會標示為 CreateOrJoin,而可從現有的交易內呼叫 (例如透過 IAtmGrain.Transfer),或自行呼叫。

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> Facet 透過 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);
}

所有對永續性狀態的讀取或寫入存取,都必須透過傳至交易狀態 Facet 的同步函式來執行。 這可讓交易系統以交易方式執行或取消這些作業。 若要在粒紋內使用交易狀態,請定義要保存的可序列化狀態類別,並使用 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 進行裝飾,以指示程式 Orleans 程式碼產生器產生序列化程式。
  • 具有以 IdAttribute 裝飾的 Value 屬性,可唯一識別成員。

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 用戶端時,會自動向相依性插入服務提供者註冊 ITransactionClientITransactionClient 可用來建立交易內容,以及在該內容中呼叫交易粒紋方法。 下列範例說明如何使用 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));
}

在上述用戶端程式碼中:

  • 使用 UseOrleansClient 設定了 IHostBuilder
    • IClientBuilder 使用 localhost 叢集和交易。
  • 從服務提供者擷取了 IClusterClientITransactionClient 介面。
  • fromto 變數指派了其 IAccountGrain 參考。
  • ITransactionClient 用來建立交易:
    • from 帳戶粒紋參考上呼叫 Withdraw
    • to 帳戶粒紋參考上呼叫 Deposit

除非在 transactionDelegate 中擲回了例外狀況或指定了衝突的 transactionOption,否則一律會認可交易。 雖然呼叫交易粒紋方法的建議方式是使用 ITransactionClient,但您也可以直接從其他粒紋呼叫交易粒紋方法。

從其他粒紋呼叫交易方法

粒紋介面上的交易方法可像任何其他粒紋方法一樣呼叫。 作為使用 ITransactionClient 的替代方法,下方的 AtmGrain 實作會在 IAccountGrain 介面上呼叫 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 中傳回值,就像其他粒紋呼叫一樣。 但在呼叫失敗時,將不會擲回應用程式例外狀況,而是會擲回 OrleansTransactionExceptionTimeoutException。 如果應用程式在交易期間擲回例外狀況,且該例外狀況導致交易失敗 (而不是因為其他系統失敗而失敗),則應用程式例外狀況將是 OrleansTransactionException 的內部例外狀況。

如果擲回了型別 OrleansTransactionAbortedException 的交易例外狀況,表示交易失敗,但可以重試。 若擲回了任何其他例外狀況,則表示交易處於不明狀態並終止。 由於交易是分散式作業,因此處於不明狀態的交易可能已成功、失敗或仍在進行中。 因此,建議您先等待呼叫逾時期間 (SiloMessagingOptions.SystemResponseTimeout) 結束以避免發生連鎖中止,然後再驗證狀態或重試作業。