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:呼叫是交易式的,且一律會建立新的交易內容 (而啟動新的交易),即使是在現有的交易內容中呼叫亦然。
- TransactionOption.Join:呼叫是交易式的,但只能在現有交易的內容中呼叫。
- TransactionOption.CreateOrJoin:呼叫是交易式的。 如果在交易的內容中呼叫,則會使用該內容,否則會建立新的內容。
- TransactionOption.Suppress:呼叫不是交易式的,但可從交易內呼叫。 如果在交易的內容中呼叫,內容將不會傳至呼叫。
- TransactionOption.Supported:呼叫不是交易式的,但支援交易。 如果在交易的內容中呼叫,內容將會傳至呼叫。
- TransactionOption.NotAllowed:呼叫不是交易式的,無法從交易內呼叫。 如果在交易的內容中呼叫,將會擲回 NotSupportedException。
呼叫可以標示為 TransactionOption.Create
,這表示呼叫一律會啟動其交易。 例如,ATM 粒紋中的下列 Transfer
作業一律會啟動涉及兩個參考帳戶的新交易。
namespace TransactionalExample.Abstractions;
public interface IAtmGrain : IGrainWithIntegerKey
{
[Transaction(TransactionOption.Create)]
Task Transfer(string fromId, string toId, decimal amountToTransfer);
}
帳戶粒紋的交易作業 Withdraw
和 Deposit
會標示為 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 用戶端時,會自動向相依性插入服務提供者註冊 ITransactionClient
。 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));
}
在上述用戶端程式碼中:
- 使用
UseOrleansClient
設定了IHostBuilder
。IClientBuilder
使用 localhost 叢集和交易。
- 從服務提供者擷取了
IClusterClient
和ITransactionClient
介面。 - 為
from
和to
變數指派了其IAccountGrain
參考。 ITransactionClient
用來建立交易:- 在
from
帳戶粒紋參考上呼叫Withdraw
。 - 在
to
帳戶粒紋參考上呼叫Deposit
。
- 在
除非在 transactionDelegate
中擲回了例外狀況或指定了衝突的 transactionOption
,否則一律會認可交易。 雖然呼叫交易粒紋方法的建議方式是使用 ITransactionClient
,但您也可以直接從其他粒紋呼叫交易粒紋方法。
從其他粒紋呼叫交易方法
粒紋介面上的交易方法可像任何其他粒紋方法一樣呼叫。 作為使用 ITransactionClient
的替代方法,下方的 AtmGrain
實作會在 IAccountGrain
介面上呼叫 Transfer
方法 (屬於交易方法)。
請考慮進行 AtmGrain
實作,這樣會解析兩個參考帳戶粒紋,並且適當呼叫 Withdraw
和 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));
}
用戶端應用程式程式碼可用交易方式呼叫 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。 如果應用程式在交易期間擲回例外狀況,且該例外狀況導致交易失敗 (而不是因為其他系統失敗而失敗),則應用程式例外狀況將是 OrleansTransactionException
的內部例外狀況。
如果擲回了型別 OrleansTransactionAbortedException 的交易例外狀況,表示交易失敗,但可以重試。 若擲回了任何其他例外狀況,則表示交易處於不明狀態並終止。 由於交易是分散式作業,因此處於不明狀態的交易可能已成功、失敗或仍在進行中。 因此,建議您先等待呼叫逾時期間 (SiloMessagingOptions.SystemResponseTimeout) 結束以避免發生連鎖中止,然後再驗證狀態或重試作業。