Поделиться через


Реализация неявной транзакции с использованием области транзакций

Класс TransactionScope предоставляет простой способ пометить блок кода как участвующий в транзакции, не требуя взаимодействия с самой транзакцией. Область транзакции может автоматически выбирать и управлять окружающей транзакцией. Из-за простоты использования и эффективности рекомендуется использовать TransactionScope класс при разработке приложения транзакции.

Кроме того, вам не нужно явно подключать ресурсы к транзакции. Любой System.Transactions диспетчер ресурсов (например, SQL Server 2005) может обнаружить существование околной транзакции, созданной областью, и автоматически участвовать.

Создание области охвата транзакций

В следующем примере показано простое использование 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

Область транзакции запускается после создания нового TransactionScope объекта. Как показано в примере кода, рекомендуется создавать области с помощью инструкции using . Оператор using доступен как в C#, так и в Visual Basic, и работает как блок try...finally, чтобы обеспечить правильное освобождение области.

При создании экземпляра TransactionScope диспетчер транзакций определяет, в какой транзакции принять участие. После определения область всегда участвует в этой транзакции. Решение основано на двух факторах: присутствует ли внешняя транзакция и значение TransactionScopeOption параметра в конструкторе. Внешняя транзакция — это транзакция, в которой выполняется код. Вы можете получить ссылку на внешнюю транзакцию, вызвав статическое Transaction.Current свойство Transaction класса. Дополнительные сведения об использовании этого параметра см. в разделе "Управление потоком транзакций с помощью TransactionScopeOption " этой статьи.

Завершение области транзакции

Когда ваше приложение завершает всю работу, которую требуется выполнить в транзакции, необходимо вызвать всего один раз метод TransactionScope.Complete, чтобы сообщить диспетчеру транзакций, что можно зафиксировать транзакцию. Очень рекомендуется поставить вызов Complete в качестве последней инструкции в блоке using .

Если не вызвать этот метод, транзакция прерывается, так как диспетчер транзакций интерпретирует это как системный сбой, что эквивалентно выбросу исключения в области транзакции. Однако вызов этого метода не гарантирует фиксацию транзакции. Это просто способ информирования диспетчера транзакций о вашем состоянии. После вызова Complete метода вы больше не сможете получить доступ к внешней транзакции с помощью Current свойства, и попытка сделать это приведет к возникновению исключения.

TransactionScope Если объект изначально создал транзакцию, фактическая работа фиксации транзакции диспетчером транзакций происходит после последней строки кода в блокеusing. Если транзакция не создана, фиксация происходит каждый раз, когда Commit вызывается владельцем объекта CommittableTransaction. На этом этапе диспетчер транзакций вызывает диспетчеры ресурсов и сообщает им о фиксации или откате в зависимости от того, был ли Complete метод вызван объектом TransactionScope .

Инструкция using обеспечивает вызов метода Dispose объекта TransactionScope даже в случае возникновения исключения. Метод Dispose помечает конец области транзакции. Исключения, возникающие после вызова этого метода, могут не повлиять на транзакцию. Этот метод также восстанавливает внешнюю транзакцию к предыдущему состоянию.

Генерируется TransactionAbortedException, если область создает транзакцию, и транзакция отклоняется. Вызывается, TransactionInDoubtException если диспетчер транзакций не может достичь решения о фиксации. Исключение не возникает, если транзакция зафиксирована.

Откат транзакции

Если вы хотите отменить транзакцию, не следует вызывать метод Complete в области транзакции. Например, можно вызвать исключение в области. Транзакция, в которой она участвует, будет отменена.

Управление потоком транзакций с помощью TransactionScopeOption

Область транзакций может быть вложена путем вызова метода, который использует TransactionScope изнутри метода, имеющего собственную область, как это происходит с методом RootMethod в следующем примере.

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

Основная область транзакций называется корневой областью.

Класс TransactionScope предоставляет несколько перегруженных конструкторов, которые принимают перечисление типа TransactionScopeOption, который определяет поведение транзакций области.

Объект TransactionScope имеет три варианта:

  • Присоединитесь к внешней транзакции или создайте новую, если она не существует.

  • Будьте новой корневой областью, то есть запустите новую транзакцию и убедитесь, что эта транзакция будет новой внешней транзакцией внутри своей собственной области.

  • Не принимать участие в транзакции вообще. В результате отсутствует контекстная транзакция.

Если контекст инстанцируется с Required и присутствует контекстная транзакция, контекст присоединяется к этой транзакции. Если, с другой стороны, нет внешней транзакции, область создает новую транзакцию и становится корневой областью. Это значение по умолчанию. Когда используется Required, код внутри области не должен изменять свое поведение, является ли он корневым или лишь участвует в окружающей транзакции. Она должна работать одинаково в обоих случаях.

Если область видимости создана с RequiresNew, она всегда является корневой областью видимости. Она запускает новую транзакцию, а ее транзакция становится новой внешней транзакцией внутри области.

Если область создаётся с помощью Suppress, она никогда не участвует в транзакции, независимо от того, существует ли окружная транзакция. Область, созданная с этим значением, всегда использует null в качестве сопутствующей транзакции.

Приведенные выше параметры приведены в следующей таблице.

TransactionScopeOption Внешняя транзакция Область принимает участие в
Обязательно нет Новая транзакция (будет корневой)
Требуется новое нет Новая транзакция (будет корневой)
Подавлять нет Нет транзакции
Обязательно Да Внешняя транзакция
Требуется новое Да Новая транзакция (будет корневой)
Подавлять Да Нет транзакции

TransactionScope При присоединении объекта к существующей внешней транзакции удаление объекта области может не завершить транзакцию, если область не прерывает транзакцию. Если внешняя транзакция была создана корневой областью, только когда корневая область будет удалена, вызывается Commit для транзакции. Если транзакция была создана вручную, транзакция заканчивается, когда она прерывается или фиксируется ее создателем.

В следующем примере показан TransactionScope объект, создающий три вложенных объекта области, каждый из которых создается с другим TransactionScopeOption значением.

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))
    {
        //...  
    }
}

В примере показан блок кода без какой-либо внешней транзакции, создающий новую область (scope1с Required). scope1 Эта область является корневой областью, так как она создает новую транзакцию (Транзакция A) и делает Транзакцию A фоновую. Scope1 затем создает три дополнительных объекта, каждый из которых имеет другое TransactionScopeOption значение. Например, scope2 создается с помощью Required, и так как существует фоновая транзакция, именно она присоединяет первую транзакцию, созданную scope1. Обратите внимание, что scope3 является корневой областью новой транзакции, а scope4 не имеет фоновой транзакции.

Хотя значение TransactionScopeOptionRequiredпо умолчанию и чаще всего используется, каждое из других значений имеет свое уникальное назначение.

Нетрaнзaкционнoй код внутри oбласти транзaкций

Suppress полезно, если вы хотите сохранить операции, выполняемые разделом кода, и не хотите прервать внешнюю транзакцию, если операции завершаются сбоем. Например, если требуется выполнять ведение журнала или аудит, или публиковать события подписчикам независимо от того, подтверждается ли ваша сопутствующая транзакция или отменяется. Это значение позволяет иметь раздел кода, отличный от транзакций, внутри области транзакции, как показано в следующем примере.

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
}

Голосование внутри вложенной области

Хотя вложенная область может присоединиться к внешней транзакции корневой области, вызов Complete в вложенной области не влияет на корневую область. Транзакция будет зафиксирована только в том случае, если все области, от корневой области до последней вложенной области, проголосовать за фиксацию транзакции. Не вызывая Complete во вложенной области, вы повлияете на корневую область, так как внешняя транзакция будет немедленно прервана.

Установка времени ожидания TransactionScope

Некоторые перегруженные конструкторы TransactionScope принимают значение типа TimeSpan, которое используется для управления временем ожидания транзакции. Время ожидания равно нулю означает бесконечное время ожидания. Бесконечное время ожидания полезно в основном для отладки, когда вы хотите изолировать проблему в бизнес-логике, выполняя код пошагово, и вы не хотите, чтобы транзакция, которую вы отлаживаете, завершилась по истечении времени в процессе поиска проблемы. Будьте очень осторожны с использованием бесконечного времени ожидания во всех других случаях, так как он переопределяет механизм защиты от взаимоблокировок транзакций.

Обычно устанавливают время ожидания TransactionScope на значения, отличающиеся от значений по умолчанию, в двух случаях. Первое — во время разработки, когда вы хотите проверить способ обработки прерванных транзакций вашим приложением. Настроив тайм-аут на небольшое значение (например, одну миллисекунду), вы вызовете сбой транзакции и таким образом сможете проверить код обработки ошибок. Второй случай, когда вы задаете значение времени ожидания меньше, чем по умолчанию, возникает, если вы считаете, что область задействована в конкуренции за ресурсы, что приводит к взаимоблокировкам. В этом случае необходимо прервать транзакцию как можно скорее и не ожидать истечения срока ожидания по умолчанию.

Когда область присоединяется к сопутствующей транзакции, но указывает меньшее время ожидания, чем то, которое установлено для сопутствующей транзакции, новое, более короткое время ожидания применяется к TransactionScope объекту, и область должна заканчиваться в течение установленного более короткого времени, или транзакция автоматически прерывается. Если время ожидания вложенной области превышает время ожидания окружающей транзакции, оно не влияет.

Настройка уровня изоляции TransactionScope

Некоторые перегруженные конструкторы TransactionScope принимают структуру типа TransactionOptions для указания уровня изоляции в дополнение к значению времени ожидания. По умолчанию транзакция выполняется с заданным уровнем Serializableизоляции. Выбор уровня изоляции, отличного от Serializable, обычно применяется в системах с интенсивными операциями чтения. Для этого требуется твердое понимание теории обработки транзакций и семантики самой транзакции, проблем параллелизма и последствий для согласованности системы.

Кроме того, не все диспетчеры ресурсов поддерживают все уровни изоляции, и они могут принять участие в транзакции на более высоком уровне, чем настроенная.

Каждый уровень изоляции, кроме того Serializable , подвержен несогласованности, возникающей из-за других транзакций, обращаюющихся к одной и той же информации. Разница между различными уровнями изоляции заключается в том, как используются блокировки чтения и записи. Блокировка может храниться только в том случае, если транзакция обращается к данным в диспетчере ресурсов, или она может храниться до тех пор, пока транзакция не будет зафиксирована или прервана. Первый лучше подходит для пропускной способности, последний для согласованности. Два типа блокировок и два типа операций (чтение и запись) дают четыре основных уровня изоляции. См. IsolationLevel для получения дополнительной информации.

При использовании вложенных объектов все вложенные TransactionScope области должны быть настроены для использования точно одного уровня изоляции, если они хотят присоединиться к внешней транзакции. Если вложенный TransactionScope объект пытается присоединиться к фоновой транзакции, но указывает другой уровень изоляции, выдаётся ArgumentException исключение.

Взаимодействие с COM+

При создании нового TransactionScope экземпляра можно использовать EnterpriseServicesInteropOption перечисление в одном из конструкторов, чтобы указать, как взаимодействовать с COM+. Дополнительные сведения об этом см. в разделе "Взаимодействие с корпоративными службами и транзакциями COM+".

См. также