System.Transactions 與 SQL Server 的整合

適用於: .NET Framework .NET .NET 標準

下載 ADO.NET

.NET 包括可透過 System.Transactions 命名空間進行存取的交易架構。 此架構會以在 .NET 中 (包括 ADO.NET) 完全整合的方式公開交易。

除了可程式性增強功能之外,System.Transactions 和 ADO.NET 還可以一起運作,以在您使用交易時協調最佳化。 可升級的交易是一種輕量型(本機)交易,可視需要自動升級為完整分散式交易。

Microsoft SqlClient Data Provider for SQL Server 在您使用 SQL Server 時支援可升級交易。 除非確實需要額外負荷,否則可升級交易不會引發分散式交易的額外負荷。 可升級交易會自動進行,無需開發人員介入。

建立可提升交易

Microsoft SqlClient Data Provider for SQL Server 支援可升級交易,其係透過 System.Transactions 命名空間中的類別來處理。 可升級交易會延後建立分散式交易,直到確實需要時才建立,以此最佳化分散式交易。 如果只需要一個資源管理員,則不會發生分散式交易。

注意

在部分信任案例中,當某筆交易提升至分散式交易時, DistributedTransactionPermission 就是必要項目。

可提升的交易案例

分散式交易通常要耗用大量的系統資源,並由「Microsoft 分散式交易協調器 (MS DTC)」進行管理,其整合了交易中會存取的所有資源管理者。 可提升的異動是 System.Transactions 異動的一種特殊形式,可將工作有效地委派給簡單的 SQL Server 異動。 System.Transactions、Microsoft.Data.SqlClient 及 SQL Server 會協調處理交易時所涉及的工作,並視需要將其提升為完全分散式交易。

使用可提升交易的好處是:當以作用中的 TransactionScope 交易開啟連接,且未開啟其他連接時,會將交易認可為輕量型交易,不會產生完全分散式交易的額外負擔。

連接字串關鍵字

ConnectionString 屬性支援關鍵字 Enlist,它表示 Microsoft.Data.SqlClient 是否會偵測到交易內容,並自動在分散式交易中登記連接。 如果 Enlist=true,則會在開啟之執行緒的目前交易內容中自動登記連接。 如果 Enlist=false,則 SqlClient 連接將不會與分散式交易進行互動。 Enlist 的預設值為 True。 如果未在連接字串中指定 Enlist ,則若在開啟連接時偵測到連接,便會自動在分散式交易中登記該連接。

SqlConnection 連線字串中的 Transaction Binding 關鍵字會控制該連線與已登錄的 System.Transactions 交易的關聯。 這也可以透過 TransactionBinding 的 SqlConnectionStringBuilder屬性取得。

下表說明可能出現的值。

關鍵字 描述
隱式解除綁定 預設值。 當交易結束時,連線會與交易分離,並切換回自動提交模式。
明確解綁 該連線會持續連結至該交易,直到交易關閉為止。 如果相關聯的交易並非使用中或與 Current不符,連接將會失敗。

使用 TransactionScope

TransactionScope 類別可藉由在分散式交易中隱含地編列連接,讓程式碼區塊可進行交易。 離開區塊之前,您必須呼叫 Complete 區塊結尾的 TransactionScope 方法。 離開區塊會叫用 Dispose 方法。 如果擲回了導致程式碼離開其範圍的例外狀況,則該交易會視為已中止。

建議您使用 using 區塊,以確保結束 using 區塊時,會在 Dispose 物件上呼叫 TransactionScope 。 未提交或回復擱置中的交易,可能會大幅降低效能,因為 TransactionScope 的預設逾時時間為一分鐘。 如果您不使用 using 陳述式,則必須在 Try 區塊中執行所有工作,並明確呼叫 Dispose 區塊中的 Finally 方法。

如果 TransactionScope中發生例外狀況,則會將交易標記為不一致並放棄。 當 TransactionScope 釋放時,將會回復。 如果沒有發生任何例外狀況,參與交易便會提交。

注意

TransactionScope 類別預設會建立一筆 IsolationLevel 為 Serializable 的交易。 根據應用程式,您可能會考慮降低隔離等級,以避免應用程式中發生劇烈的競爭現象。

注意

建議您在分散式交易內僅執行更新、插入及刪除作業,因為它們會耗用大量的資料庫資源。 SELECT 陳述可能會不必要地鎖定資料庫資源,而且在某些情況下,您可能必須對 SELECT 使用交易。 除非涉及其他已交易的資源管理者,否則所有非資料庫工作都應在交易範圍外執行。 雖然交易範圍中的例外狀況會使交易無法認可,但 TransactionScope 類別並未提供任何機制,可回復您的程式碼在交易本身範圍之外所做的任何變更。 如果您需要在交易回復時執行某些動作,就必須自行撰寫 IEnlistmentNotification 介面的實作,並明確加入該交易。

範例

您需要參考 System.Transactions.dll,才能使用 System.Transactions 。

下列函式示範如何針對兩個不同的 SQL Server 執行個體建立可升級交易;這兩個執行個體分別由兩個不同的 SqlConnection 物件表示,且包在 TransactionScope 區塊中。

下列程式碼會使用 TransactionScope 陳述式建立 using 區塊,並開啟第一個連線,這樣會自動在 TransactionScope 中加以登錄。

一開始交易登記為輕量型交易,而不是完全分散式交易。 只有當第一個連接中的命令沒有擲回例外狀況時,第二個連接才會登記於 TransactionScope 中。 開啟第二個連接時,交易會自動提升為完全分散式交易。

稍後會呼叫 Complete 方法,只有在未擲回任何例外時,該方法才會提交交易。 如果在 TransactionScope 區塊中的任何位置擲回了例外狀況,就不會呼叫 Complete,而且當 TransactionScope 在其 using 區塊結束時遭到處置時,分散式交易就會回復原狀。

using System;
using System.Transactions;
using Microsoft.Data.SqlClient;

class Program
{
    static void Main(string[] args)
    {
        string connectionString = "Data Source = localhost; Integrated Security = true; Initial Catalog = AdventureWorks";

        string commandText1 = "INSERT INTO Production.ScrapReason(Name) VALUES('Wrong size')";
        string commandText2 = "INSERT INTO Production.ScrapReason(Name) VALUES('Wrong color')";

        int result = CreateTransactionScope(connectionString, connectionString, commandText1, commandText2);

        Console.WriteLine("result = " + result);
    }

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

        // Create the TransactionScope in which to execute the commands, guaranteeing  
        // that both commands will commit or roll back as a single unit of work.  
        using (TransactionScope scope = new TransactionScope())
        {
            using (SqlConnection connection1 = new SqlConnection(connectString1))
            {
                try
                {
                    // 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 by opening connection2
                    // only when there is a chance that the transaction can commit.
                    using (SqlConnection connection2 = new SqlConnection(connectString2))
                        try
                        {
                            // The transaction is promoted 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);
                        }
                        catch (Exception ex)
                        {
                            // Display information that command2 failed.  
                            writer.WriteLine("returnValue for command2: {0}", returnValue);
                            writer.WriteLine("Exception Message2: {0}", ex.Message);
                        }
                }
                catch (Exception ex)
                {
                    // Display information that command1 failed.  
                    writer.WriteLine("returnValue for command1: {0}", returnValue);
                    writer.WriteLine("Exception Message1: {0}", ex.Message);
                }
            }

            // If an exception has been thrown, Complete will not
            // be called and the transaction is rolled back.  
            scope.Complete();
        }

        // The returnValue is greater than 0 if the transaction committed.  
        if (returnValue > 0)
        {
            writer.WriteLine("Transaction was committed.");
        }
        else
        {
            // You could write additional business logic here, notify the caller by  
            // throwing a TransactionAbortedException, or log the failure.  
            writer.WriteLine("Transaction rolled back.");
        }

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

        return returnValue;
    }
}