Bagikan melalui


Menerapkan Transaksi Implisit menggunakan Cakupan Transaksi

Kelas TransactionScope menyediakan cara sederhana untuk menandai blok kode sebagai berpartisipasi dalam transaksi, tanpa mengharuskan Anda untuk berinteraksi dengan transaksi itu sendiri. Lingkup transaksi dapat memilih dan mengelola transaksi ambient secara otomatis. Karena kemudahan penggunaan dan efisiensinya, Anda disarankan untuk menggunakan kelas TransactionScope saat mengembangkan aplikasi transaksi.

Selain itu, Anda tidak perlu meminta sumber daya secara eksplisit dengan transaksi. Manajer sumber daya System.Transactions mana pun (seperti SQL Server 2005) dapat mendeteksi keberadaan transaksi sekitar yang dibuat oleh ruang lingkup dan mendaftar secara otomatis.

Membuat ruang lingkup transaksi

Contoh berikut menunjukkan penggunaan sederhana dari kelas TransactionScope.

// This function takes arguments for 2 connection strings and commands to create a transaction
// involving two SQL Servers. It returns a value > 0 if the transaction is committed, 0 if the
// transaction is rolled back. To test this code, you can connect to two different databases
// on the same server by altering the connection string, or to another 3rd party RDBMS by
// altering the code in the connection2 code block.
static public int CreateTransactionScope(
    string connectString1, string connectString2,
    string commandText1, string commandText2)
{
    // Initialize the return value to zero and create a StringWriter to display results.
    int returnValue = 0;
    System.IO.StringWriter writer = new System.IO.StringWriter();

    try
    {
        // Create the TransactionScope to execute the commands, guaranteeing
        // that both commands can commit or roll back as a single unit of work.
        using (TransactionScope scope = new TransactionScope())
        {
            using (SqlConnection connection1 = new SqlConnection(connectString1))
            {
                // Opening the connection automatically enlists it in the
                // TransactionScope as a lightweight transaction.
                connection1.Open();

                // Create the SqlCommand object and execute the first command.
                SqlCommand command1 = new SqlCommand(commandText1, connection1);
                returnValue = command1.ExecuteNonQuery();
                writer.WriteLine("Rows to be affected by command1: {0}", returnValue);

                // If you get here, this means that command1 succeeded. By nesting
                // the using block for connection2 inside that of connection1, you
                // conserve server and network resources as connection2 is opened
                // only when there is a chance that the transaction can commit.
                using (SqlConnection connection2 = new SqlConnection(connectString2))
                {
                    // The transaction is escalated to a full distributed
                    // transaction when connection2 is opened.
                    connection2.Open();

                    // Execute the second command in the second database.
                    returnValue = 0;
                    SqlCommand command2 = new SqlCommand(commandText2, connection2);
                    returnValue = command2.ExecuteNonQuery();
                    writer.WriteLine("Rows to be affected by command2: {0}", returnValue);
                }
            }

            // The Complete method commits the transaction. If an exception has been thrown,
            // Complete is not  called and the transaction is rolled back.
            scope.Complete();
        }
    }
    catch (TransactionAbortedException ex)
    {
        writer.WriteLine("TransactionAbortedException Message: {0}", ex.Message);
    }

    // Display messages.
    Console.WriteLine(writer.ToString());

    return returnValue;
}
'  This function takes arguments for 2 connection strings and commands to create a transaction 
'  involving two SQL Servers. It returns a value > 0 if the transaction is committed, 0 if the 
'  transaction is rolled back. To test this code, you can connect to two different databases 
'  on the same server by altering the connection string, or to another 3rd party RDBMS  
'  by altering the code in the connection2 code block.
Public Function CreateTransactionScope( _
  ByVal connectString1 As String, ByVal connectString2 As String, _
  ByVal commandText1 As String, ByVal commandText2 As String) As Integer

    ' Initialize the return value to zero and create a StringWriter to display results.
    Dim returnValue As Integer = 0
    Dim writer As System.IO.StringWriter = New System.IO.StringWriter

    Try
        ' Create the TransactionScope to execute the commands, guaranteeing
        '  that both commands can commit or roll back as a single unit of work.
        Using scope As New TransactionScope()
            Using connection1 As New SqlConnection(connectString1)
                ' Opening the connection automatically enlists it in the 
                ' TransactionScope as a lightweight transaction.
                connection1.Open()

                ' Create the SqlCommand object and execute the first command.
                Dim command1 As SqlCommand = New SqlCommand(commandText1, connection1)
                returnValue = command1.ExecuteNonQuery()
                writer.WriteLine("Rows to be affected by command1: {0}", returnValue)

                ' If you get here, this means that command1 succeeded. By nesting
                ' the using block for connection2 inside that of connection1, you
                ' conserve server and network resources as connection2 is opened
                ' only when there is a chance that the transaction can commit.   
                Using connection2 As New SqlConnection(connectString2)
                    ' The transaction is escalated to a full distributed
                    ' transaction when connection2 is opened.
                    connection2.Open()

                    ' Execute the second command in the second database.
                    returnValue = 0
                    Dim command2 As SqlCommand = New SqlCommand(commandText2, connection2)
                    returnValue = command2.ExecuteNonQuery()
                    writer.WriteLine("Rows to be affected by command2: {0}", returnValue)
                End Using
            End Using

            ' The Complete method commits the transaction. If an exception has been thrown,
            ' Complete is called and the transaction is rolled back.
            scope.Complete()
        End Using
    Catch ex As TransactionAbortedException
        writer.WriteLine("TransactionAbortedException Message: {0}", ex.Message)
    End Try

    ' Display messages.
    Console.WriteLine(writer.ToString())

    Return returnValue
End Function

Cakupan transaksi dimulai setelah Anda membuat objek TransactionScope baru. Seperti yang diilustrasikan dalam contoh kode, Anda disarankan untuk membuat cakupan dengan pernyataan using. Pernyataan using tersedia baik dalam C# dan Visual Basic, dan berfungsi seperti blok try...finally untuk memastikan bahwa ruang lingkup dibuang dengan benar.

Saat Anda membuat instance TransactionScope, manajer transaksi menentukan transaksi mana yang akan diikuti. Setelah ditentukan, ruang lingkup selalu berpartisipasi dalam transaksi itu. Keputusan didasarkan pada dua faktor: apakah ada transaksi ambient dan nilai parameter TransactionScopeOption di konstruktor. Transaksi ambient adalah transaksi di mana kode Anda dieksekusi. Anda dapat memperoleh referensi ke transaksi sekitar dengan memanggil properti Transaction.Current statis dari kelas Transaction. Untuk informasi selengkapnya tentang bagaimana parameter ini digunakan, lihat bagian Mengelola alur transaksi menggunakan TransactionScopeOption dari topik ini.

Menyelesaikan ruang lingkup transaksi

Ketika aplikasi Anda menyelesaikan semua pekerjaan yang ingin dilakukan dalam suatu transaksi, Anda harus memanggil metode TransactionScope.Complete hanya sekali untuk memberi tahu manajer transaksi bahwa itu dapat diterima untuk melakukan transaksi. Ini adalah praktik yang sangat baik untuk menempatkan panggilan ke Complete sebagai pernyataan terakhir di blok using.

Gagal memanggil metode ini membatalkan transaksi, karena manajer transaksi menafsirkan ini sebagai kegagalan sistem, atau setara dengan pengecualian yang dilemparkan dalam lingkup transaksi. Namun, memanggil metode ini tidak menjamin bahwa transaksi akan dilakukan. Ini hanyalah cara untuk memberi tahu manajer transaksi tentang status Anda. Setelah memanggil metode Complete, Anda tidak dapat lagi mengakses transaksi sekitar dengan menggunakan properti Current, dan mencoba melakukannya akan menghasilkan pengecualian yang dilemparkan.

Jika objek TransactionScope membuat transaksi pada awalnya, pekerjaan setruenya dari melakukan transaksi oleh manajer transaksi terjadi setelah baris kode terakhir di blok using. Jika tidak membuat transaksi, komit terjadi setiap kali Commit dipanggil oleh pemilik objek CommittableTransaction. Pada saat itu, manajer transaksi memanggil pengelola sumber daya dan memberi tahu mereka untuk melakukan atau mengembalikan, berdasarkan apakah metode Complete dipanggil pada objek TransactionScope.

Pernyataan using memastikan bahwa metode Dispose dari objek TransactionScope dipanggil bahkan jika pengecualian terjadi. Metode Dispose menandai akhir dari cakupan transaksi. Pengecualian yang terjadi setelah memanggil metode ini mungkin tidak mempengaruhi transaksi. Metode ini juga mengembalikan transaksi ambient ke keadaan sebelumnya.

TransactionAbortedException dilemparkan jika cakupan membuat transaksi, dan transaksi dibatalkan. Sebuah TransactionInDoubtException dilemparkan jika manajer transaksi tidak dapat mencapai keputusan Komit. Tidak ada pengecualian yang dilemparkan jika transaksi dilakukan.

Mengembalikan transaksi

Jika Anda ingin mengembalikan transaksi, Anda tidak boleh memanggil metode Complete dalam cakupan transaksi. Misalnya, Anda dapat melempar pengecualian dalam ruang lingkup. Transaksi di mana ia berpartisipasi akan dibatalkan.

Mengelola aliran transaksi menggunakan TransactionScopeOption

Lingkup transaksi dapat disarangkan dengan memanggil metode yang menggunakan TransactionScope dari dalam metode yang menggunakan cakupannya sendiri, seperti halnya metode RootMethod dalam contoh berikut,

void RootMethod()
{
    using(TransactionScope scope = new TransactionScope())
    {
        /* Perform transactional work here */
        SomeMethod();
        scope.Complete();
    }
}

void SomeMethod()
{
    using(TransactionScope scope = new TransactionScope())
    {
        /* Perform transactional work here */
        scope.Complete();
    }
}

Lingkup transaksi paling atas disebut sebagai root scope.

Kelas TransactionScope menyediakan beberapa konstruktor kelebihan beban yang menerima enumerasi jenis TransactionScopeOption, yang mendefinisikan perilaku transaksional cakupan.

Objek TransactionScope memiliki tiga opsi:

  • Bergabunglah dengan transaksi ambient, atau buat yang baru jika tidak ada.

  • Jadilah lingkup root baru, yaitu, memulai transaksi baru dan menjadikan transaksi itu sebagai transaksi ambient baru di dalam cakupannya sendiri.

  • Tidak ikut serta dalam suatu transaksi sama sekali. Hasilnya, tidak ada transaksi ambient.

Jika cakupan dibuat dengan Required, dan ada transaksi sekitar, cakupan akan bergabung dengan transaksi tersebut. Jika, di sisi lain, tidak ada transaksi ambient, maka ruang lingkup membuat transaksi baru, dan menjadi ruang lingkup root. Ini adalah nilai default. Saat Required digunakan, kode di dalam cakupan tidak perlu berperilaku berbeda apakah itu root atau hanya bergabung dengan transaksi sekitar. Ini harus beroperasi secara identik dalam kedua kasus.

Jika cakupan dibuat dengan RequiresNew, itu selalu merupakan cakupan root. Ini memulai transaksi baru, dan transaksinya menjadi transaksi ambient baru di dalam ruang lingkup.

Jika cakupan dibuat dengan Suppress, cakupannya tidak pernah mengambil bagian dalam transaksi, terlepas dari apakah ada transaksi sekitar. Cakupan yang dibuat dengan nilai ini selalu memiliki null sebagai transaksi ambiennya.

Opsi di atas diringkas dalam tabel berikut.

Opsi Lingkup Transaksi Transaksi Ambient Ruang lingkup mengambil bagian dalam
Diperlukan No Transaksi Baru (akan menjadi root)
Membutuhkan Baru No Transaksi Baru (akan menjadi root)
Menyembunyikan No Tidak Ada Transaksi
Wajib Ya Transaksi Ambient
Membutuhkan Baru Ya Transaksi Baru (akan menjadi root)
Menyembunyikan Ya Tidak Ada Transaksi

Saat objek TransactionScope bergabung dengan transaksi sekitar yang ada, membuang objek cakupan tidak dapat mengakhiri transaksi, kecuali jika ruang lingkup membatalkan transaksi. Jika transaksi ambient dibuat oleh root scope, hanya ketika root scope dibuang, Commit akan dipanggil pada transaksi. Jika transaksi dibuat secara manual, transaksi berakhir ketika dibatalkan, atau dilakukan oleh pembuatnya.

Contoh berikut menunjukkan objek TransactionScope yang membuat tiga objek cakupan bersarang, masing-masing dibuat dengan nilai TransactionScopeOption yang berbeda.

using(TransactionScope scope1 = new TransactionScope())
//Default is Required
{
    using(TransactionScope scope2 = new TransactionScope(TransactionScopeOption.Required))
    {
        //...
    }

    using(TransactionScope scope3 = new TransactionScope(TransactionScopeOption.RequiresNew))
    {
        //...  
    }
  
    using(TransactionScope scope4 = new TransactionScope(TransactionScopeOption.Suppress))
    {
        //...  
    }
}

Contoh menunjukkan blok kode tanpa transaksi sekitar yang membuat cakupan baru (scope1) dengan Required. Cakupan scope1 adalah ruang lingkup root karena membuat transaksi baru (Transaksi A) dan menjadikan Transaksi A sebagai transaksi sekitar. Scope1 kemudian membuat tiga objek lagi, masing-masing dengan nilai TransactionScopeOption yang berbeda. Misalnya, scope2 dibuat dengan Required, dan karena ada transaksi sekitar, ia bergabung dengan transaksi pertama yang dibuat oleh scope1. Perhatikan bahwa scope3 adalah cakupan utama dari transaksi baru, dan bahwa scope4 tidak memiliki transaksi sekitar.

Meskipun nilai default dan yang paling umum digunakan dari TransactionScopeOption adalah Required, masing-masing nilai lainnya memiliki tujuan yang unik.

Kode non-transaksional di dalam lingkup transaksi

Suppress berguna saat Anda ingin mempertahankan operasi yang dilakukan oleh bagian kode, dan tidak ingin membatalkan transaksi sekitar jika operasi gagal. Misalnya, saat Anda ingin melakukan logging atau operasi audit, atau saat Anda ingin memublikasikan peristiwa ke pelanggan terlepas dari apakah transaksi sekitar Anda dilakukan atau dibatalkan. Nilai ini memungkinkan Anda untuk memiliki bagian kode non-transaksional di dalam lingkup transaksi, seperti yang ditunjukkan pada contoh berikut.

using(TransactionScope scope1 = new TransactionScope())
{
    try
    {
        //Start of non-transactional section
        using(TransactionScope scope2 = new
            TransactionScope(TransactionScopeOption.Suppress))  
        {  
            //Do non-transactional work here  
        }  
        //Restores ambient transaction here
   }
   catch {}  
   //Rest of scope1
}

Memilih di dalam lingkup bersarang

Meskipun cakupan berlapis dapat bergabung dengan transaksi sekitar cakupan akar, panggilan Complete dalam cakupan berlapis tidak berpengaruh pada cakupan akar. Transaksi hanya akan dilakukan jika semua cakupan, dari cakupan akar hingga cakupan berlapis terakhir, pilih untuk melakukan transaksi. Tidak memanggil Complete dalam cakupan berlapis akan memengaruhi cakupan akar, karena transaksi sekitar akan segera dibatalkan.

Mengatur batas waktu TransactionScope

Beberapa konstruktor TransactionScope yang kelebihan beban menerima nilai jenis TimeSpan, yang digunakan untuk mengontrol batas waktu transaksi. Batas waktu yang diatur ke nol berarti batas waktu tak terbatas. Batas waktu tak terbatas sebagian besar berguna untuk debugging, ketika Anda ingin mengisolasi masalah dalam logika bisnis Anda dengan menelusuri kode Anda, dan Anda tidak ingin transaksi yang Anda debug habis saat Anda mencoba untuk menemukan masalah. Berhati-hatilah dalam menggunakan nilai batas waktu tak terbatas dalam semua kasus lain, karena ini mengesampingkan pengamanan terhadap kebuntuan transaksi.

Anda biasanya mengatur batas waktu TransactionScope ke nilai selain default dalam dua kasus. Yang pertama adalah selama pengembangan, ketika Anda ingin menguji cara aplikasi Anda menangani transaksi yang dibatalkan. Dengan mengatur batas waktu ke nilai kecil (seperti satu milidetik), Anda menyebabkan transaksi Anda gagal dan dengan demikian dapat mengamati kode penanganan kesalahan Anda. Kasus kedua di mana Anda menetapkan nilai menjadi kurang dari batas waktu default adalah ketika Anda yakin bahwa ruang lingkup terlibat dalam pertikaian sumber daya, yang mengakibatkan kebuntuan. Dalam hal ini, Anda ingin membatalkan transaksi sesegera mungkin dan tidak menunggu batas waktu default berakhir.

Saat cakupan bergabung dengan transaksi sekitar tetapi menentukan batas waktu yang lebih kecil daripada yang diatur ke transaksi sekitar, batas waktu baru yang lebih pendek diterapkan pada objek TransactionScope, dan ruang lingkup harus berakhir dalam waktu bersarang yang ditentukan, atau transaksi otomatis dibatalkan. Jika batas waktu cakupan bersarang lebih dari transaksi sekitar, itu tidak berpengaruh.

Mengatur tingkat isolasi TransactionScope

Beberapa konstruktor TransactionScope yang kelebihan beban menerima struktur jenis TransactionOptions untuk menentukan tingkat isolasi, selain nilai batas waktu. Secara default, transaksi dijalankan dengan tingkat isolasi yang diatur ke Serializable. Memilih tingkat isolasi selain Serializable biasanya digunakan untuk sistem intensif baca. Ini membutuhkan pemahaman yang kuat tentang teori pemrosesan transaksi dan semantik transaksi itu sendiri, masalah konkurensi yang terlibat, dan konsekuensi untuk konsistensi sistem.

Selain itu, tidak semua pengelola sumber daya mendukung semua tingkat isolasi, dan mereka dapat memilih untuk mengambil bagian dalam transaksi pada tingkat yang lebih tinggi daripada yang dikonfigurasi.

Setiap tingkat isolasi selain Serializable rentan terhadap inkonsistensi akibat transaksi lain yang mengakses informasi yang sama. Perbedaan antara tingkat isolasi yang berbeda adalah cara kunci baca dan tulis digunakan. Kunci dapat ditahan hanya ketika transaksi mengakses data di pengelola sumber daya, atau dapat ditahan sampai transaksi dilakukan atau dibatalkan. Yang pertama lebih baik untuk throughput, yang terakhir untuk konsistensi. Dua jenis kunci dan dua jenis operasi (baca/tulis) memberikan empat tingkat isolasi dasar. Lihat IsolationLevel untuk informasi lebih lanjut.

Saat menggunakan objek TransactionScope bersarang, semua cakupan bersarang harus dikonfigurasi untuk menggunakan tingkat isolasi yang sama persis jika ingin bergabung dengan transaksi sekitar. Jika objek TransactionScope bersarang mencoba bergabung dengan transaksi sekitar namun menentukan tingkat isolasi yang berbeda, ArgumentException akan dilemparkan.

Interop dengan COM+

Saat Anda membuat instans TransactionScope baru, Anda dapat menggunakan enumerasi EnterpriseServicesInteropOption di salah satu konstruktor untuk menentukan cara berinteraksi dengan COM+. Untuk informasi selengkapnya tentang ini, lihat Interoperabilitas dengan Layanan Perusahaan dan Transaksi COM+.

Lihat juga