Bagikan melalui


Orleans Transaksi

Orleans mendukung transaksi ACID terdistribusi terhadap status biji-bijian persisten. Transaksi diimplementasikan menggunakan Microsoft.Orleans. Paket Transaksi NuGet. Kode sumber untuk aplikasi sampel dalam artikel ini terdiri dari empat proyek:

  • Abstraksi: Pustaka kelas yang berisi antarmuka biji-bijian dan kelas bersama.
  • Biji-bijian: Pustaka kelas yang berisi implementasi biji-bijian.
  • Server: Aplikasi konsol yang menggunakan abstraksi dan biji-bijian pustaka kelas dan bertindak sebagai Orleans silo.
  • Klien: Aplikasi konsol yang menggunakan pustaka kelas abstraksi yang mewakili Orleans klien.

Siapkan

Orleans transaksi adalah keikutsertaan. Silo dan klien keduanya harus dikonfigurasi untuk menggunakan transaksi. Jika tidak dikonfigurasi, setiap panggilan ke metode transaksional pada implementasi biji-bijian OrleansTransactionsDisabledExceptionakan menerima . Untuk mengaktifkan transaksi pada silo, hubungi SiloBuilderExtensions.UseTransactions pembangun host silo:

var builder = Host.CreateDefaultBuilder(args)
    UseOrleans((context, siloBuilder) =>
    {
        siloBuilder.UseTransactions();
    });

Demikian juga, untuk mengaktifkan transaksi pada klien, panggil ClientBuilderExtensions.UseTransactions pembangun host klien:

var builder = Host.CreateDefaultBuilder(args)
    UseOrleansClient((context, clientBuilder) =>
    {
        clientBuilder.UseTransactions();
    });

Penyimpanan status transaksi

Untuk menggunakan transaksi, Anda perlu mengonfigurasi penyimpanan data. Untuk mendukung berbagai penyimpanan data dengan transaksi, abstraksi ITransactionalStateStorage<TState> penyimpanan digunakan. Abstraksi ini khusus untuk kebutuhan transaksi, tidak seperti penyimpanan biji-bijian generik (IGrainStorage). Untuk menggunakan penyimpanan khusus transaksi, konfigurasikan silo menggunakan implementasi apa pun dari ITransactionalStateStorage, seperti Azure (AddAzureTableTransactionalStateStorage).

Misalnya, pertimbangkan konfigurasi pembangun host berikut:

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();

Untuk tujuan pengembangan, jika penyimpanan khusus transaksi tidak tersedia untuk datastore yang IGrainStorage Anda butuhkan, Anda dapat menggunakan implementasi sebagai gantinya. Untuk status transaksional apa pun yang tidak memiliki penyimpanan yang dikonfigurasi, transaksi akan mencoba melakukan failover ke penyimpanan biji-bijian menggunakan jembatan. Mengakses status transaksional melalui jembatan ke penyimpanan biji-bijian kurang efisien dan mungkin tidak didukung di masa mendatang. Oleh karena itu, rekomendasinya adalah hanya menggunakan ini untuk tujuan pengembangan.

Antarmuka biji-bijian

Agar biji-bijian mendukung transaksi, metode transaksional pada antarmuka biji-bijian harus ditandai sebagai bagian dari transaksi menggunakan TransactionAttribute. Atribut perlu menunjukkan bagaimana panggilan biji-bijian berperilaku di lingkungan transaksi sebagaimana dirinci dengan nilai berikut TransactionOption :

  • TransactionOption.Create: Panggilan bersifat transaksional dan akan selalu membuat konteks transaksi baru (memulai transaksi baru), bahkan jika dipanggil dalam konteks transaksi yang ada.
  • TransactionOption.Join: Panggilan bersifat transaksi tetapi hanya dapat dipanggil dalam konteks transaksi yang ada.
  • TransactionOption.CreateOrJoin: Panggilan bersifat transaksi. Jika dipanggil dalam konteks transaksi, transaksi akan menggunakan konteks tersebut, jika tidak, itu akan membuat konteks baru.
  • TransactionOption.Suppress: Panggilan tidak transaksi tetapi dapat dipanggil dari dalam transaksi. Jika dipanggil dalam konteks transaksi, konteks tidak akan diteruskan ke panggilan.
  • TransactionOption.Supported: Panggilan bukan transaksi tetapi mendukung transaksi. Jika dipanggil dalam konteks transaksi, konteks akan diteruskan ke panggilan.
  • TransactionOption.NotAllowed: Panggilan tidak transaksi dan tidak dapat dipanggil dari dalam transaksi. Jika dipanggil dalam konteks transaksi, transaksi akan melemparkan NotSupportedException.

Panggilan dapat ditandai sebagai TransactionOption.Create, yang berarti panggilan akan selalu memulai transaksinya. Misalnya, Transfer operasi dalam butir ATM di bawah ini akan selalu memulai transaksi baru yang melibatkan dua akun yang direferensikan.

namespace TransactionalExample.Abstractions;

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

Operasi Withdraw transaksional dan Deposit pada butir akun ditandai TransactionOption.Join, menunjukkan bahwa mereka hanya dapat dipanggil dalam konteks transaksi yang ada, yang akan terjadi jika mereka dipanggil selama IAtmGrain.Transfer. Panggilan GetBalance ditandai CreateOrJoin sehingga dapat dipanggil dari dalam transaksi yang ada, seperti melalui IAtmGrain.Transfer, atau sendiri.

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();
}

Pertimbangan penting

Tidak dapat ditandai sebagai transaksi sebagai panggilan seperti itu OnActivateAsync memerlukan penyiapan yang tepat sebelum panggilan. Ini hanya ada untuk API aplikasi biji-bijian. Ini berarti bahwa upaya untuk membaca status transaksional sebagai bagian dari metode ini akan melemparkan pengecualian dalam runtime.

Implementasi biji-bijian

Implementasi biji-bijian perlu menggunakan faset ITransactionalState<TState> untuk mengelola status biji-bijian melalui transaksi 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);
}

Semua akses baca atau tulis ke status bertahan harus dilakukan melalui fungsi sinkron yang diteruskan ke faset status transaksional. Ini memungkinkan sistem transaksi untuk melakukan atau membatalkan operasi ini secara transaksional. Untuk menggunakan status transaksional dalam biji-bijian, Anda menentukan kelas status yang dapat diserialisasikan untuk dipertahankan dan mendeklarasikan status transaksional dalam konstruktor biji-bijian dengan TransactionalStateAttribute. Yang terakhir mendeklarasikan nama status dan, secara opsional, penyimpanan status transaksional mana yang akan digunakan. Untuk informasi selengkapnya, lihat Penyetelan.

[AttributeUsage(AttributeTargets.Parameter)]
public class TransactionalStateAttribute : Attribute
{
    public TransactionalStateAttribute(string stateName, string storageName = null)
    {
        // ...
    }
}

Sebagai contoh, Balance objek status didefinisikan sebagai berikut:

namespace TransactionalExample.Abstractions;

[GenerateSerializer]
public record class Balance
{
    [Id(0)]
    public decimal Value { get; set; } = 1_000;
}

Objek status sebelumnya:

  • Dihiasi dengan GenerateSerializerAttribute untuk menginstruksikan Orleans generator kode untuk menghasilkan serializer.
  • Memiliki Value properti yang dihiasi dengan IdAttribute untuk mengidentifikasi anggota secara unik.

Objek Balance status kemudian digunakan dalam AccountGrain implementasi sebagai berikut:

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);
}

Penting

Butir transaksian harus ditandai dengan ReentrantAttribute untuk memastikan bahwa konteks transaksi diteruskan dengan benar ke panggilan biji-bijian.

Dalam contoh sebelumnya, TransactionalStateAttribute digunakan untuk menyatakan bahwa balance parameter konstruktor harus dikaitkan dengan status transaksional bernama "balance". Dengan deklarasi ini, Orleans akan menyuntikkan ITransactionalState<TState> instans dengan status yang dimuat dari penyimpanan status transaksi bernama "TransactionStore". Status dapat dimodifikasi melalui PerformUpdate atau dibaca melalui PerformRead. Infrastruktur transaksi akan memastikan bahwa setiap perubahan yang dilakukan sebagai bagian dari transaksi, bahkan di antara beberapa biji-bijian yang didistribusikan melalui kluster Orleans , semuanya akan diterapkan atau semuanya dibatalkan setelah menyelesaikan panggilan biji-bijian yang membuat transaksi (IAtmGrain.Transfer dalam contoh sebelumnya).

Memanggil metode transaksi dari klien

Cara yang disarankan untuk memanggil metode biji-bijian transaksi adalah dengan menggunakan ITransactionClient. ITransactionClient secara otomatis terdaftar dengan penyedia layanan injeksi dependensi saat klien dikonfigurasiOrleans. digunakan untuk membuat konteks transaksi dan untuk memanggil metode biji-bijian transaksi dalam konteks tersebut ITransactionClient . Contoh berikut menunjukkan cara menggunakan ITransactionClient untuk memanggil metode biji-bijian transaksi.

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));
}

Dalam kode klien sebelumnya:

  • dikonfigurasi IHostBuilder dengan UseOrleansClient.
    • menggunakan IClientBuilder pengklusteran dan transaksi localhost.
  • Antarmuka IClusterClient dan ITransactionClient diambil dari penyedia layanan.
  • Variabel from dan to diberi referensinya IAccountGrain .
  • ITransactionClient digunakan untuk membuat transaksi, memanggil:
    • Withdrawfrom pada referensi butir akun.
    • Depositto pada referensi butir akun.

Transaksi selalu dilakukan kecuali ada pengecualian yang dilemparkan dalam transactionDelegate atau kontradiktif transactionOption yang ditentukan. Meskipun cara yang disarankan untuk memanggil metode biji-bijian transaksi adalah dengan menggunakan ITransactionClient, Anda juga dapat memanggil metode biji-bijian transaksi langsung dari biji-bijian lain.

Memanggil metode transaksi dari butir lain

Metode transaksional pada antarmuka biji-bijian disebut seperti metode biji-bijian lainnya. Sebagai pendekatan alternatif menggunakan ITransactionClient, AtmGrain implementasi di bawah ini memanggil Transfer metode (yang transaksional) pada IAccountGrain antarmuka.

AtmGrain Pertimbangkan implementasi, yang menyelesaikan dua butir akun yang dirujuk dan melakukan panggilan yang sesuai ke Withdraw dan 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));
}

Kode aplikasi klien Anda dapat memanggil AtmGrain.Transfer dengan cara transaksi sebagai berikut:

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();

Dalam panggilan sebelumnya, IAtmGrain digunakan untuk mentransfer 100 unit mata uang dari satu akun ke akun lainnya. Setelah transfer selesai, kedua akun dikueri untuk mendapatkan saldo mereka saat ini. Transfer mata uang, serta kedua kueri akun, dilakukan sebagai transaksi ACID.

Seperti yang ditunjukkan dalam contoh sebelumnya, transaksi dapat mengembalikan nilai dalam Task, seperti panggilan biji-bijian lainnya. Tetapi setelah kegagalan panggilan, mereka tidak akan melemparkan pengecualian aplikasi melainkan OrleansTransactionException atau TimeoutException. Jika aplikasi melemparkan pengecualian selama transaksi dan pengecualian itu menyebabkan transaksi gagal (dibandingkan dengan gagal karena kegagalan sistem lain), pengecualian aplikasi akan menjadi pengecualian dalam dari OrleansTransactionException.

Jika pengecualian transaksi dilemparkan dari jenis OrleansTransactionAbortedException, transaksi gagal dan dapat dicoba kembali. Pengecualian lain yang dilemparkan menunjukkan bahwa transaksi dihentikan dengan status yang tidak diketahui. Karena transaksi adalah operasi terdistribusi, transaksi dalam keadaan tidak diketahui bisa berhasil, gagal, atau masih berlangsung. Untuk alasan ini, disarankan untuk mengizinkan periode batas waktu panggilan (SiloMessagingOptions.SystemResponseTimeout) untuk melewati, untuk menghindari pembatakan berjenjang, sebelum memverifikasi status atau mencoba kembali operasi.