Локальные транзакции Local Transactions

В этой статье

В ADO.NET ADO.NET транзакции используются для связи нескольких задач, чтобы они выполнялись как одно целое. Transactions in ADO.NET ADO.NET are used when you want to bind multiple tasks together so that they execute as a single unit of work. Например, пусть приложение выполняет две задачи. For example, imagine that an application performs two tasks. Во-первых, оно заносит в таблицу сведения о заказе. First, it updates a table with order information. Во-вторых, обновляет таблицу, содержащую список товаров на складе, списывая заказанные элементы. Second, it updates a table that contains inventory information, debiting the items ordered. При сбое любой задачи, затем оба обновления откатываются. If either task fails, then both updates are rolled back.

Определение типа транзакции Determining the Transaction Type

Транзакция считается локальной транзакции при состоит из одной фазы и обрабатывается непосредственно базой данных. A transaction is considered to be a local transaction when it is a single-phase transaction and is handled by the database directly. Транзакция считается распределенной транзакции при координируется монитором транзакций и используется для разрешения транзакций резервные механизмы (например, двухфазную фиксацию). A transaction is considered to be a distributed transaction when it is coordinated by a transaction monitor and uses fail-safe mechanisms (such as two-phase commit) for transaction resolution.

Для выполнения локальных транзакций каждый поставщик данных платформы .NET Framework .NET Framework имеет свой собственный объект Transaction . Each of the .NET Framework .NET Framework data providers has its own Transaction object for performing local transactions. Если требуется выполнить транзакцию в базе данных SQL Server, выбирается транзакция System.Data.SqlClient . If you require a transaction to be performed in a SQL Server database, select a System.Data.SqlClient transaction. Для транзакции Oracle используйте поставщик System.Data.OracleClient . For an Oracle transaction, use the System.Data.OracleClient provider. Кроме того, DbTransaction класс, который доступен для написания независимого от поставщика кода, который требуется транзакций. In addition, there is a DbTransaction class that is available for writing provider-independent code that requires transactions.

Примечание

Транзакции наиболее эффективны, если они выполняются на сервере. Transactions are most efficient when they are performed on the server. При работе с базой данных SQL Server, интенсивно использующей явные транзакции, следует рассмотреть возможность их записи в виде хранимых процедур при помощи инструкции Transact-SQL BEGIN TRANSACTION. If you are working with a SQL Server database that makes extensive use of explicit transactions, consider writing them as stored procedures using the Transact-SQL BEGIN TRANSACTION statement.

Выполнение транзакций с использованием одного соединения Performing a Transaction Using a Single Connection

В ADO.NET ADO.NET транзакции управляются объектом Connection . In ADO.NET ADO.NET , you control transactions with the Connection object. Инициировать транзакцию можно с помощью метода BeginTransaction . You can initiate a local transaction with the BeginTransaction method. После начала транзакции при помощи свойства Transaction объекта Command к ней можно прикрепить команду. Once you have begun a transaction, you can enlist a command in that transaction with the Transaction property of a Command object. Затем в зависимости от успеха или ошибки компонентов транзакции можно зафиксировать или откатить изменения, сделанные в источнике данных. You can then commit or roll back modifications made at the data source based on the success or failure of the components of the transaction.

Примечание

Метод EnlistDistributedTransaction не должен использоваться для локальной транзакции. The EnlistDistributedTransaction method should not be used for a local transaction.

Область действия транзакции ограничена соединением. The scope of the transaction is limited to the connection. В следующем примере выполняется явная транзакция, состоящая из двух отдельных команд в блоке try . The following example performs an explicit transaction that consists of two separate commands in the try block. Команды выполняют инструкции INSERT для таблицы Production.ScrapReason в образце базы данных AdventureWorks в SQL Server, которые будут зафиксированы при отсутствии исключений. The commands execute INSERT statements against the Production.ScrapReason table in the AdventureWorks SQL Server sample database, which are committed if no exceptions are thrown. При возникновении исключения код в блоке catch произведет откат транзакции. The code in the catch block rolls back the transaction if an exception is thrown. При отмене транзакции или обрыве соединения до выполнения транзакции она откатывается автоматически. If the transaction is aborted or the connection is closed before the transaction has completed, it is automatically rolled back.

Пример Example

Чтобы осуществить транзакцию, выполните указанные ниже действия. Follow these steps to perform a transaction.

    Вызовите метод BeginTransaction объекта SqlConnection для отметки начала транзакции. Call the BeginTransaction method of the SqlConnection object to mark the start of the transaction. Метод BeginTransaction возвращает ссылку на транзакцию. The BeginTransaction method returns a reference to the transaction. Эта ссылка назначается объектам SqlCommand , прикрепленным к транзакции. This reference is assigned to the SqlCommand objects that are enlisted in the transaction.

    Присвойте объект Transaction свойству Transaction объекта SqlCommand . Assign the Transaction object to the Transaction property of the SqlCommand to be executed. Исключение вызывается, если команда выполняется при соединении с активной транзакцией, а объект Transaction не был назначен свойству Transaction объекта Command . If a command is executed on a connection with an active transaction, and the Transaction object has not been assigned to the Transaction property of the Command object, an exception is thrown.

    Выполните требуемые команды. Execute the required commands.

    Для выполнения транзакции вызовите метод Commit объекта SqlTransaction , для завершения транзакции вызовите метод Rollback . Call the Commit method of the SqlTransaction object to complete the transaction, or call the Rollback method to end the transaction. Транзакция откатывается, если соединение закрывается или пропадает до выполнения метода Commit либо Rollback . If the connection is closed or disposed before either the Commit or Rollback methods have been executed, the transaction is rolled back.

Следующий пример кода демонстрирует транзакционную логику, используемую ADO.NET ADO.NET с Microsoft SQL Server. The following code example demonstrates transactional logic using ADO.NET ADO.NET with Microsoft SQL Server.

Using (SqlConnection connection = new SqlConnection(connectionString)) { connection.Open(); // Start a local transaction. SqlTransaction sqlTran = connection.BeginTransaction(); // Enlist a command in the current transaction. SqlCommand command = connection.CreateCommand(); command.Transaction = sqlTran; try { // Execute two separate commands. command.CommandText = "INSERT INTO Production.ScrapReason(Name) VALUES("Wrong size")"; command.ExecuteNonQuery(); command.CommandText = "INSERT INTO Production.ScrapReason(Name) VALUES("Wrong color")"; command.ExecuteNonQuery(); // Commit the transaction. sqlTran.Commit(); Console.WriteLine("Both records were written to database."); } catch (Exception ex) { // Handle the exception if the transaction fails to commit. Console.WriteLine(ex.Message); try { // Attempt to roll back the transaction. sqlTran.Rollback(); } catch (Exception exRollback) { // Throws an InvalidOperationException if the connection // is closed or the transaction has already been rolled // back on the server. Console.WriteLine(exRollback.Message); } } } Using connection As New SqlConnection(connectionString) connection.Open() " Start a local transaction. Dim sqlTran As SqlTransaction = connection.BeginTransaction() " Enlist a command in the current transaction. Dim command As SqlCommand = connection.CreateCommand() command.Transaction = sqlTran Try " Execute two separate commands. command.CommandText = _ "INSERT INTO Production.ScrapReason(Name) VALUES("Wrong size")" command.ExecuteNonQuery() command.CommandText = _ "INSERT INTO Production.ScrapReason(Name) VALUES("Wrong color")" command.ExecuteNonQuery() " Commit the transaction sqlTran.Commit() Console.WriteLine("Both records were written to database.") Catch ex As Exception " Handle the exception if the transaction fails to commit. Console.WriteLine(ex.Message) Try " Attempt to roll back the transaction. sqlTran.Rollback() Catch exRollback As Exception " Throws an InvalidOperationException if the connection " is closed or the transaction has already been rolled " back on the server. Console.WriteLine(exRollback.Message) End Try End Try End Using

См. также See also

  • Транзакции и параллельность Transactions and Concurrency
  • Распределенные транзакции Distributed Transactions
  • Интеграция System.Transactions с SQL Server System.Transactions Integration with SQL Server
  • Центр разработчиков наборов данных и управляемых поставщиков ADO.NET ADO.NET Managed Providers and DataSet Developer Center

Класс 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. 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) Catch ex As ApplicationException writer.WriteLine("ApplicationException Message: {0}", ex.Message) End Try " Display messages. Console.WriteLine(writer.ToString()) Return returnValue End Function // 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); } catch (ApplicationException ex) { writer.WriteLine("ApplicationException Message: {0}", ex.Message); } // Display messages. Console.WriteLine(writer.ToString()); return returnValue; }

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

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

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

Когда приложение завершает все операции, подлежащие выполнению в транзакции, следует вызвать метод 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.

Сводка рассмотренных выше значений представлена в следующей таблице.

После присоединения объекта 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) и делает ее внешней транзакцией. Затем Scope1 создает три дополнительных объекта с разными значениями TransactionScopeOption. Например, объект scope2 создается со значением Required; поскольку существует внешняя транзакция, этот объект присоединяется к первой транзакции, созданной объектом scope1. Обратите внимание, что scope3 является корневой областью новой транзакции, а scope4 не имеет внешней транзакции.

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

Значение 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+

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

Транзакции

Транзакция - это набор операций, который должен либо выполниться успешно, либо не выполниться, причем как единое целое. Целью транзакции является гарантия того, что данные всегда находятся в корректном, согласованном состоянии.

Например, рассмотрим транзакцию, которая передает $1000 со счета A на счет B. Ясно, что здесь присутствует две операции:

    снять $1000 со счета А;

    добавить $1000 на счет B.

Предположим, что приложение успешно выполнило шаг 1, но из-за какой-то ошибки шаг 2 завершился сбоем. Это ведет к несогласованию данных, поскольку общая сумма денег в системе теперь не точна. Пропало $1000.

Транзакции позволяют избежать проблем подобного рода, поскольку гарантируют, что изменения будут зафиксированы в источнике данных только в том случае, если все шаги пройдут успешно. Поэтому в рассматриваемом примере, если шаг 2 сорвется, то изменения, выполненные на шаге 1, не будут зафиксированы в базе данных. Это гарантирует, что система останется в одном из двух корректных состояний - начальном (когда деньги не переведены) или конечном (когда деньги дебетованы с одного счета и кредитованы на другой).

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

Атомарность (Atomic)

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

Согласованность (Consistent)

Транзакция переводит лежащую в основе базу данных из одного стабильного состояния в другое.

Изолированность (Isolated)

Каждая транзакция - независимая сущность. Одна транзакция не должна влиять на другие, запущенные одновременно с ней.

Устойчивость (Durable)

Изменения, которые происходят во время транзакции, сохраняются постоянно на каком-то носителе (обычно на жестком диске), прежде чем транзакция будет объявлена успешной. Поддерживается ведение журналов, чтобы база данных могла быть восстановлена в корректное состояние, даже в случае сбоя оборудования или сети.

Обратите внимание, что эти идеальные характеристики транзакции не всегда полностью достижимы. Одна из проблем связана с обеспечением изоляции. Реляционная СУБД должна блокировать данные, чтобы другие пользователи не имели к ним доступа, пока транзакция выполняется. Чем больше используется блокировок, и чем они крупнее, тем выше шансы того, что пользователь не сможет выполнить другую работу, пока транзакция находится в процессе выполнения. Другими словами, иногда приходится идти на компромисс между степенью параллелизма работы пользователей и изоляцией.

Транзакции и приложения ASP.NET

В приложениях ASP.NET можно использовать три базовых типа транзакций. Ниже представлен их список (от наименее до наиболее затратных):

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

    Инициированные клиентом (ADO.NET) транзакции . Эти транзакции управляются программно кодом вашей веб-страницы ASP.NET. "За кулисами" они применяют те же команды, что и транзакции хранимых процедур, но код использует ряд объектов ADO.NET, которые скрывают детали. Недостатком является необходимость в нескольких обращениях к базе данных, чтобы запустить и зафиксировать транзакцию.

    Транзакции COM+ . Эти транзакции обрабатываются исполняющей средой COM+ на основе декларативных атрибутов, добавленных вами в код. Транзакции COM+ используют протокол двухфазной фиксации и всегда подразумевают наличие дополнительных накладных расходов. Они требуют создания отдельного класса служебного компонента.

    В общем объекты COM+ хороший выбор только в том случае, если транзакция охватывает несколько транзакционных диспетчеров ресурсов, поскольку COM+ включает встроенную поддержку распределенных транзакций. Например, отдельная транзакция COM+ может охватывать взаимодействие с базой SQL Server и базой Oracle.

Несмотря на то что ADO.NET предоставляет хорошую поддержку транзакций, вы не всегда должны их использовать. Фактически всякий раз, когда применяется какого-то рода транзакция, автоматически привносятся накладные расходы. К тому же транзакции предполагают некоторого рода блокировку строк таблицы. Поэтому транзакции, используемые без необходимости, могут нанести ущерб масштабируемости приложения.

Чтобы достичь наилучшего результата, при реализации транзакций нужно следовать перечисленным ниже рекомендациям:

    Сохраняйте транзакции насколько возможно короткими.

    Избегайте возврата данных запросом SELECT посреди транзакции. В идеале вы должны вернуть данные перед запуском транзакции. Это сократит объем данных, заблокированных транзакцией.

    Если вы извлекаете записи, то извлекайте только те строки, которые необходимы, чтобы сократить количество блокировок.

    Где только возможно, реализуйте транзакции внутри хранимых процедур, а не используйте транзакции ADO.NET. В результате транзакции будут стартовать и завершаться быстрее, т.к. серверу базы данных не придется взаимодействовать с клиентом (веб-приложением).

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

    По возможности избегайте обновлений, которые затрагивают большое количество записей.

В ADO.NET также поддерживается высокоуровневая модель распространяемых транзакций. Однако распространяемая транзакция - это не какой-то новый тип транзакции, а просто способ создания инициируемой клиентом транзакции, которая при необходимости может автоматически расширяться до транзакции COM+. Вы не должны использовать распространяемые транзакции, если только действительно в них не нуждаетесь, потому что трудно предсказать их влияние на производительность и масштабируемость окончательных решений.

Запомните эмпирическое правило: применяйте транзакции только тогда, когда операция того требует. Например, если вы просто выбираете записи из базы данных либо инициируете запрос, то транзакции не нужны. С другой стороны, если вы вставляете запись Order, которая связана с последовательностью зависимых записей OrderItem, то транзакция может понадобиться. Вообще транзакции никогда не требуются для одиночных команд, таких как индивидуальные операторы UPDATE, DELETE или INSERT, поскольку они по определению транзакционны.

Транзакции хранимых процедур

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

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

Код хранимых процедур варьируется в зависимости от используемой базы данных, но большинство реляционных СУБД поддерживают SQL-оператор BEGIN TRANSACTION . После запуска транзакции все последующие операторы рассматриваются как ее часть. Завершается транзакция с помощью оператора COMMIT или ROLLBACK . Если этого не сделать, транзакция будет автоматически отменена.

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

CREATE Procedure TransferAmount (@Amount Money @ID_A int @ID_B int) AS BEGIN TRANSACTION UPDATE Accounts SET Balance = Balance + @Amount WHERE AccountID = @ID_A IF (@@ERROR > 0) GOTO PROBLEM UPDATE Accounts SET Balance = Balance - @Amount WHERE AccountID = @ID_B IF (@@ERROR > 0) GOTO PROBLEM -- Никаких проблем обнаружено не было COMMIT RETURN -- Код для обработки ошибок PROBLEM: ROLLBACK; RAISERROR("Обновление выполнить не удалось", 16, 1)

В приведенном примере применяются средства ограниченной обработки ошибок Transact-SQL (вариант SQL, реализованный в SQL Server). При использовании значения @@ERROR в Transact-SQL следует соблюдать осторожность и проверять его немедленно после каждой операции. Это связано с тем, что @@ERROR сбрасывается в 0 при успешном завершении оператора SQL. В результате, если первое обновление потерпит неудачу, а второе выполнится успешно, @@ERROR вернет 0. В этой точке проверять его уже слишком поздно.

Инициированные клиентом транзакции ADO.NET

Большинство поставщиков данных ADO.NET включают поддержку баз данных. Транзакции стартуют через объект Connection вызовом метода BeginTransaction() . Этот метод возвращает специфичный для поставщика объект Transaction , используемый для управления транзакцией. Все классы Transaction реализуют интерфейс IDbTransaction . Поставщики включают классы наподобие SqlTransaction, OleDbTransaction, OracleTransaction и т.д. реализующие этот интерфейс.

Класс Transaction предоставляет два ключевых метода:

Commit()

Этот метод указывает завершение транзакции и помещение выполненных изменений в источнике данных.

Rollback()

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

Обычно метод Commit() используется в конце операции. Однако если в процессе возникнет любое исключение, должен быть вызван Rollback(). Рассмотрим пример вставки двух записей в таблицу Employees:

Protected void Page_Load(object sender, EventArgs e) { string connectionString = WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString; SqlConnection con = new SqlConnection(connectionString); SqlCommand cmd1 = new SqlCommand("INSERT INTO Employees (LastName, FirstName) VALUES ("Petrov", "Vasya")", con); SqlCommand cmd2 = new SqlCommand("INSERT INTO Employees (LastName, FirstName) VALUES ("Ivanov", "Vadim")", con); SqlTransaction transaction = null; try { // Открыть соединение и создать транзакцию con.Open(); transaction = con.BeginTransaction(); // Включить в транзакцию две команды cmd1.Transaction = transaction; cmd2.Transaction = transaction; // Выполнить обе команды cmd1.ExecuteNonQuery(); cmd2.ExecuteNonQuery(); // Зафиксировать транзакцию transaction.Commit(); } catch { // В случае ошибки отменить транзакцию transaction.Rollback(); } finally { // В любом случае закрыть соединение с базой данных con.Close(); } }

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

Вместо использования отдельных объектов команд можно также выполнить один и тот же объект дважды, просто по ходу модифицируя его свойство CommandText (если это динамический оператор SQL) или значения его параметров (если это параметризованная команда). Например, если команда вставляет новую запись, такой подход можно применить для вставки двух записей в одной транзакции.

Чтобы протестировать свойство отката (отмены) транзакции, вставьте следующую строку непосредственно перед вызовом метода Commit() в предыдущем примере:

Throw new ApplicationException();

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

Хотя транзакции ADO.NET вращаются вокруг объектов Command и Transaction, лежащие в основе команды не отличаются от тех, что применяются в транзакциях хранимых процедур. Например, когда вызывается метод BeginTransaction() с поставщиком данных SQL Server, он отправляет базе данных команду BEGIN TRANSACTION.

Транзакция должна быть завершена как можно скорее (запущена как можно позже и завершена как можно раньше). К тому же активная транзакция блокирует различные задействованные ресурсы, поэтому следует выбирать только те строки таблиц, которые действительно необходимы.

Уровни изоляции

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

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

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

В SQL Server на самом деле поддерживается несколько типов блокировок, работающих совместно, чтобы предотвратить взаимные блокировки и другие неприятные ситуации. За дополнительной информацией о блокировках обращайтесь в руководство SQL Server Books Online Help, которое устанавливается вместе с SQL Server.

В хранимых процедурах SQL Server можно устанавливать уровни изоляции, используя команду SET TRANSACTION ISOLATION LEVEL . В ADO.NET можно передать значение перечисления IsolationLevel перегруженному методу Connection.BeginTransaction(). В таблице ниже показаны его возможные значения:

Значения перечисления IsolationLevel
Значение Описание
ReadUncommitted

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

ReadCommitted

Разделяемые блокировки удерживаются, пока данные читаются транзакцией. Это исключает случаи недействительного чтения, но данные могут быть изменены до завершения транзакции. Это может привести к невоспроизводимым чтениям или фантомным записям. Этот уровень изоляции в SQL Server используется по умолчанию

Snapshot

Сохраняет копию данных, к которым обращается транзакция. В результате транзакция не может видеть изменений, выполненных другими транзакциями. Этот подход ограничивает блокировки, поскольку даже если другие транзакции удерживают блокировки на этих данных, транзакция с таким уровнем изоляции будет способна читать копии данных. Этот уровень изоляции поддерживается только в SQL Server 2005 и последующих версиях и должен быть включен через параметр уровня базы данных

RepeatableRead

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

Serializable

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

В этой таблице введена терминология, связанная с базами данных, которая требует пояснений:

Грязное чтение (dirty read)

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

Невоспроизводимое чтение (unrepeatable read)

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

Фантомная строка (phantom row)

Фантомная строка - это строка, которая не появляется при начальном чтении, но появляется, когда те же данные читаются вновь в той же транзакции. Это может произойти, если другой пользователь вставляет запись до завершения вашей транзакции. Чтобы предотвратить появление фантомных строк, когда ваша транзакция выполняет запрос, сервер базы данных должен применять блокировку диапазона на основе конструкции WHERE этого запроса.

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

Уровни изоляции в приведенной выше таблице располагаются от наименьшей степени блокировки до наибольшей. Уровень по умолчанию, ReadCommitted, является хорошим компромиссом для большинства транзакций. В следующей таблице описано поведение блокировки для разных уровней изоляции:

Точки сохранения

Всякий раз, когда производится откат транзакции, аннулируются результаты всех команд, выполненных с момента старта транзакции. Но как быть, если вы хотите откатить только часть текущей транзакции? В SQL Server для этого предназначено средство, которое называется точками сохранения (savepoints) .

Точки сохранения - это метки, которые работают подобно книжным закладкам. Вы отмечаете определенную точку в потоке транзакции, и затем можете выполнять ее откат до этой точки. Точка сохранения устанавливается с помощью метода Transaction.Save() . Обратите внимание, что метод Save() доступен только в классе SqlTransaction, поскольку не является частью стандартного интерфейса IDbTransaction.

Ниже демонстрируется концепция использования точки сохранения:

// Запустить транзакцию SqlTransaction tran = con.BeginTransaction(); // (Включить и выполнить некоторые команды в транзакции.) // Отметить точку сохранения tran.Save("CompletedInsert") ; // (Включить и выполнить еще какие-то команды в транзакции.) // Если необходимо, выполнить откат до точки сохранения tran.Rollback("CompletedInsert"); // Зафиксировать или откатить всю транзакцию tran.Commit();

Обратите внимание на использование метода Rollback() с точкой сохранения в качестве параметра. Если вы хотите откатить всю транзакцию, просто пропустите этот параметр. При откате транзакции до точки сохранения все точки сохранения, определенные после данной, теряются. Если они нужны, их придется установить заново.

Существует два основных вида сделок: транзакции подключения и внешние транзакции. Сделка соединения (например, SqlTransaction) привязана непосредственно к соединению db (например, SqlConnection), что означает, что вам нужно продолжать передавать соединение - в некоторых случаях ОК, но не разрешает «создавать/использовать/выпускать», использование и не разрешает работу с несколькими db. Пример (отформатирован для пробела):

Using (IDbTransaction tran = conn.BeginTransaction()) { try { //your code tran.Commit(); } catch { tran.Rollback(); throw; } }

Не слишком грязно, но ограничивается нашей связью «conn». Если мы хотим обратиться к различным методам, нам нужно передать «conn».

Альтернативой является внешняя транзакция; новый в.NET 2.0, объект TransactionScope (система.Transactions.dll) позволяет использовать по всему диапазону операций (подходящие провайдеры будут автоматически зачисляться в транзакцию с окружающими). Это позволяет легко встраиваться в существующий (не транзакционный) код и разговаривать с несколькими провайдерами (хотя DTC будет задействован, если вы поговорите с более чем одним).

Например:

Using(TransactionScope tran = new TransactionScope()) { CallAMethodThatDoesSomeWork(); CallAMethodThatDoesSomeMoreWork(); tran.Complete(); }

Обратите внимание, что эти два метода могут обрабатывать свои собственные соединения (open/use/close/dispose), но они беззвучно станут частью внешней транзакции без необходимости пропускать что-либо.

Если ваши ошибки кода, Dispose() будет вызываться без Complete (), поэтому он будет откат. Ожидаемое вложение и т. Д. Поддерживается, хотя вы не можете откатить внутреннюю транзакцию, но завершите внешнюю транзакцию: если кто-то недоволен, транзакция прерывается.

Другим преимуществом TransactionScope является то, что он не привязан к базам данных; любой поставщик транзакций может использовать его. WCF, например. Или есть даже некоторые модели объектов, совместимых с TransactionScope (например, классы.NET с возможностью отката - возможно, проще, чем память, хотя я никогда не использовал этот подход самостоятельно).

В общем, очень, очень полезный объект.

Некоторые оговорки:

  • В SQL Server 2000 TransactionScope немедленно переключится на DTC; это исправлено в SQL Server 2005 и выше, оно может использовать LTM (намного меньше служебных), пока вы не поговорите с 2 источниками и т. д., когда он повышен до DTC.
  • Существует сбой , что означает, что вам может потребоваться настроить строка подключения

Protected void Button1_Click(object sender, EventArgs e) { using (SqlConnection connection1 = new SqlConnection("Data Source=.\\SQLEXPRESS;AttachDbFilename=|DataDirectory|\\Database.mdf;Integrated Security=True;User Instance=True")) { connection1.Open(); //Start a local transaction. SqlTransaction sqlTran = connection1.BeginTransaction(); //Enlist a command in the current transaction. SqlCommand command = connection1.CreateCommand(); command.Transaction = sqlTran; try { //Execute two separate commands. command.CommandText = "insert into (drname,drspecialization,drday) values ("a","b","c")"; command.ExecuteNonQuery(); command.CommandText = "insert into (drname,drspecialization,drday) values ("x","y","z")"; command.ExecuteNonQuery(); //Commit the transaction. sqlTran.Commit(); Label3.Text = "Both records were written to database."; } catch (Exception ex) { //Handle the exception if the transaction fails to commit. Label4.Text = ex.Message; try { //Attempt to roll back the transaction. sqlTran.Rollback(); } catch (Exception exRollback) { //Throws an InvalidOperationException if the connection //is closed or the transaction has already been rolled //back on the server. Label5.Text = exRollback.Message; } } } }

Введение

Транзакция - это последовательность операций, выполняемых как единое целое. Благодаря объединению взаимосвязанных операций в транзакцию гарантируется согласованность и целостность данных в системе, несмотря на любые ошибки, которые могли возникнуть в ходе транзакции. Для успешного выполнения транзакции необходимо успешное завершение всех ее операций.

У транзакции есть начало и конец, определяющие ее границы (transaction boundaries), внутри которых транзакция может охватывать различные процессы и компьютеры. Все ресурсы, используемые в ходе данной транзакции, считаются участвующими в этой транзакции. Для поддержания целостности используемых ресурсов транзакция должна обладать свойствами ACID: Atomicity (атомарность), Consistency (целостность), Isolation (изоляция) и Durability (отказоустойчивость). Подробнее об основах обработки транзакций см. Processing Transactions (EN) в Microsoft .NET Framework SDK и Transaction Processing в Microsoft Platform SDK.

В этой статье мы покажем, как выполнять локальные и распределенные транзакции в приложениях Microsoft .NET.

Локальные и распределенные транзакции

Локальной называется транзакция, областью действия которой является один ресурс, поддерживающий транзакции, - база данных Microsoft ® SQL Server™, очередь сообщений MSMQ и др. Например, отдельно взятая СУБД может вводить в действие правила ACID, когда в ее распоряжении имеются все данные, участвующие в транзакции. В SQL Server предусмотрен внутренний диспетчер транзакций (transaction manager), предоставляющий функциональность для фиксации (commit) и отката (rollback) транзакций.

Распределенные транзакции могут использовать гетерогенные ресурсы, поддерживающие транзакции, включать самые разнообразные операции, например выборку информации из базы данных SQL Server, считывание сообщений Message Queue Server и запись в другие базы данных. Программирование распределенных приложений упрощается программным обеспечением, способным координировать фиксацию и откат, а также восстановление данных, хранящихся в различных ресурсах. Одной из таких технологий является DTC (Microsoft Distributed Transaction Coordinator). DTC реализует протокол двухфазной фиксации (two-phase commit protocol), гарантирующий непротиворечивость результатов транзакции во всех ресурсах, участвующих в этой транзакции. DTC поддерживает только приложения, в которых реализуются совместимые с ним интерфейсы управления транзакциями. Эти приложения называются диспетчерами ресурсов (Resource Managers) (дополнительную информацию по этой теме см. в Distributed Transactions (EN) в.NET Framework Developer’s Guide). В настоящее время существует довольно много таких приложений - MSMQ, Microsoft SQL Server, Oracle, Sybase и др.

Транзакции баз данных

Вызов хранимой процедуры (stored procedure), которая заключает необходимые операции в операторы BEGIN TRANSACTION и COMMIT/ROLLBACK TRANSACTION, дает наилучшую производительность, позволяя выполнить транзакцию с разовым обменом данными с сервером (single round-trip) . Кроме того, транзакции баз данных могут быть вложенными, т. е. внутри активной транзакции можно начать выполнение новой транзакции.

В следующем фрагменте кода оператор BEGIN TRANSACTION начинает новую транзакцию. Транзакцию можно завершить двумя способами: либо фиксацией изменений в базе данных оператором COMMIT TRANSACTION, либо (при возникновении какой-либо ошибки) отменой всех изменений оператором ROLLBACK TRANSACTION.

CREATE PROCEDURE Proc1 … AS -- Начинаем транзакцию BEGIN TRANSACTION -- Выполняем операции транзакции … -- Проверяем наличие ошибок If @@Error <> 0 -- Откатываем транзакцию ROLLBACK TRANSACTION … -- Фиксируем транзакцию COMMIT TRANSACTION

Показанная ниже хранимая процедура принимает в качестве входного параметра XML-представление информации о заказе (order). Для выполнения соответствующих вставок в таблицы Orders и OrderDetails хранимая процедура загружает и анализирует XML с помощью системной хранимой процедуры sp_xmlpreparedocument . Как видно из исходного кода, все операции хранимой процедуры включены в явно выполняемую транзакцию, поэтому при неудачном завершении любой операции все внесенные изменения отменяются (откатываются).

Заметьте, что процедура устанавливает флаг XACT_ABORT в ON, указывая, что SQL Server должен автоматически откатить транзакцию, если выполнить какой-нибудь оператор не удастся.

CREATE PROCEDURE InsertOrder @Order NVARCHAR(4000) = NULL , @OrderId int Output AS SET NOCOUNT ON DECLARE @hDoc INT DECLARE @PKId INT -- Указываем, что SQL Server должен автоматически откатывать текущую -- транзакцию, если оператор Transact-SQL генерирует ошибку периода -- выполнения (run-time error). SET XACT_ABORT ON -- Начинаем транзакцию BEGIN TRANSACTION -- Загружаем и анализируем содержимое входного XML-представления -- информации о заказе, а затем помещаем его в XMLDocument EXEC sp_xml_preparedocument @hDoc OUTPUT, @Order -- Выбираем заголовок заказа из XMLDocument-узла Orders -- и вставляем его в таблицу Orders INSERT Orders(CustomerId, OrderDate, ShipToName, ShipToAddressId, OrderStatus) SELECT CustomerId, CONVERT(DateTime,OrderDate), ShipToName, ShipToAddressId, OrderStatus FROM OPENXML(@hDoc, "/NewDataSet/Orders") WITH (CustomerId int "CustomerId", OrderDate nvarchar(23) "OrderDate", ShipToName nvarchar(40) "ShipToName", ShipToAddressId int "ShipToAddressId", OrderStatus int "OrderStatus") -- Выбираем OrderId заказа, только что вставленного в таблицу Orders -- для использования при вставке позиций заказа (order details) SELECT @PKId = @@IDENTITY -- Выбираем позиции заказа из XMLDocument-узла Details -- и вставляем их в таблицу OrderDetails INSERT OrderDetails (OrderId, ItemId, UnitPrice, Quantity) SELECT @PKId as OrderId, ItemId, UnitPrice, Quantity FROM OPENXML(@hDoc, "/NewDataSet/Details") WITH (ItemId int "ItemId", UnitPrice money "UnitPrice", Quantity int "Quantity") -- Присваиваем значение выходному параметру Select @OrderId = @PKId -- Фиксируем транзакцию COMMIT TRANSACTION EXEC sp_xml_removedocument @hDoc RETURN 0 GO

Хотя такой подход обеспечивает хорошую производительность, при его использовании приходится программировать на Transact SQL, а это сложнее, чем на языке, совместимом с.NET.

Транзакции вручную

Транзакции, выполняемые вручную (manual transactions), позволяют явно управлять границами транзакции с помощью команд начала и конца транзакции. В этой модели также поддерживаются вложенные транзакции, т. е. вы можете начинать новую транзакцию в рамках активной транзакции. Расплата за возможность такого управления заключается в том, что на вас ложится бремя включения в транзакцию нужных ресурсов данных и их координация. Встроенной поддержки распределенных транзакций нет, поэтому управление распределенной транзакцией вручную потребует взять на себя массу обязанностей. Вам придется управлять каждым подключением к ресурсу и его использованием, а также реализовать поддержку свойств ACID в транзакции.

Транзакции ADO.NET, выполняемые вручную

Транзакции вручную поддерживают оба провайдера данных Microsoft ADO.NET, которые предоставляют набор объектов, позволяющих создавать соединение с хранилищем данных, начинать транзакцию, фиксировать или откатывать ее и, наконец, закрывать соединение. В своих примерах мы будем использовать управляемый ADO. NET-провайдер SQL (ADO.NET SQL managed provider).

Для выполнения операций в рамках единой транзакции нужно создать объект SQLTransaction , начать транзакцию с помощью объекта SQLConnection , добиться, чтобы все операции над базой данных проходили в этой транзакции, и зафиксировать или отменить транзакцию. Объект SQLTransaction предоставляет целый ряд свойств и методов для управления транзакцией. При успешном выполнении всех операций транзакции вы можете зафиксировать изменения в базе данных методом Commit . Для отката изменений применяется метод Rollback объекта SQLTransaction .

Для выполнения SQL-команды в транзакции свойство Transaction объекта Command необходимо установить на уже начатую транзакцию.

Visual Basic .NET

Dim conn as SQLConnection Dim cmd as SQLCommand Dim txn As SQLTransaction conn = New SQLConnection("ConnString") cmd = New SQLCommand " Открываем соединение conn.Open() " Начинаем транзакцию txn = conn.BeginTransaction() cmd.Transaction = Txn

Visual C# .NET

SQLConnection Conn = New SQLConnection("ConnString"); SQLCommand Cmd = New SQLCommand; // Открываем соединение Conn.Open(); // Начинаем транзакцию SQLTransaction Txn = Conn.BeginTransaction(); // Настраиваем свойство Transaction на транзакцию, где выполняется // SQL-команда Cmd.Transaction = Txn;

В примере, представленном ниже, мы выполняем в рамках транзакции две SQL-команды. Первая вставляет заголовок заказа (order header) в таблицу Orders и возвращает OrderId только что созданного заказа. Этот OrderId используется во второй команде, которая вставляет позиции этого заказа в таблицу OrderDetails. Транзакция отменяется, если хотя бы одна из двух команд терпит неудачу; при этом строки в базу данных не добавляются.

Visual Basic .NET

Dim conn As SqlConnection Dim cmd As SqlCommand Dim tran As SqlTransaction " Создаем новое соединение conn = New SqlConnection("ConnString") " Открываем соединение conn.Open() " Создаем объект Command cmd = New SqlCommand() " Создаем транзакцию tran = conn.BeginTransaction " Настраиваем свойство Transaction на транзакцию, где выполняется " SQL-команда cmd.Transaction = tran Try " Вставляем заголовок заказа. " Настраиваем свойства Command With cmd .CommandType = CommandType.StoredProcedure .CommandText = "InsertOrderHeader" .Connection = conn " Добавляем входные и выходные параметры.Parameters.Add("@CustomerId", SqlDbType.Int) .Parameters("@CustomerId").Direction = ParameterDirection.Input … " Устанавливаем значения параметров.Parameters("@CustomerId").Value = 1 … " Выполняем команду .ExecuteNonQuery() " Получаем OrderId добавленного заголовка заказа OrderId = .Parameters("@OrderId").Value " Очищаем параметры для следующей команды.Parameters.clear() End With " Вставляем позиции заказа " Настраиваем свойства Command With cmd .CommandType = CommandType.StoredProcedure .CommandText = "InsertOrderDetail" .Connection = conn " Добавляем параметры.Parameters.Add("@OrderId", SqlDbType.Int) .Parameters("@OrderId").SourceColumn = "OrderId" .Parameters("@OrderId").Direction = ParameterDirection.Input … " Устанавливаем значения параметров.Parameters("@OrderId").Value = OrderId .Parameters("@ItemId").Value = 100 … " Выполняем команду .ExecuteNonQuery() " Повторяем показанные выше строки для каждой позиции заказа End With " Фиксируем транзакцию tran.Commit() Catch " Откатываем транзакцию tran.Rollback() Finally " Код очистки. " Закрываем соединение. conn.Close() End Try

Как видите, две команды выполняются как часть одной транзакции. Если одна из них терпит неудачу, транзакция отменяется, и любые изменения в базе данных откатываются. Заключив код в блок try/catch/finally, вы гарантируете корректное выполнение транзакции: она фиксируется в самом конце блока try после успешного выполнения обеих SQL-команд. Любое исключение перехватывается в блоке catch, где транзакция отменяется и изменения, внесенные в ходе этой транзакции, откатываются.

Управление транзакциями через объекты ADO.NET приводит к менее эффективному блокированию, чем при использовании явных транзакций в хранимых процедурах. Причина в том, что при транзакциях ADO.NET, выполняемых вручную, требуется как минимум столько же двусторонних обменов данными с СУБД, сколько операций выполняется в транзакции плюс два обмена в ее начале и конце. Блокировки удерживаются в течение всего времени передачи вызовов из кода ADO.NET на сервер базы данных и обратно.

Транзакции MSMQ, выполняемые вручную

NET Framework предусматривает два вида поддержки транзакций MSMQ: внутреннюю (для транзакций вручную) и внешнюю (для автоматических транзакций). В первом случае в рамках транзакции возможен прием или передача нескольких сообщений. Во втором - сообщения участвуют в транзакциях DTC (Distributed Transaction Coordinator).

Транзакции MSMQ, выполняемые вручную, поддерживаются классом MessageQueueTransaction и обрабатываются исключительно ядром MSMQ. Подробности см. в статье Дункана Мак-Кензи (Duncan Mackenzie) (EN) .

Автоматические транзакции

Поддержка автоматических транзакций в.NET Framework опирается на службы MTS/COM+. COM+ использует DTC в качестве диспетчера и координатора транзакций при выполнении транзакций в распределенной среде. Это позволяет приложениям.NET выполнять транзакции, охватывающие разнообразные операции над множеством ресурсов, например вставку заказа в базу данных SQL Server, запись сообщения в очередь MSMQ (Microsoft Message Queue), отправку сообщения электронной почты и считывание информации из базы данных Oracle.

Предоставляя модель программирования на основе декларативных транзакций (declarative transactions), COM+ резко упрощает выполнение транзакций, в которых участвуют гетегрогенные ресурсы. Но учтите, что за это приходится расплачиваться снижением производительности, связанным с издержками взаимодействия DTC и COM; кроме того, поддержка вложенных транзакций отсутствует.

Страницы ASP.NET, методы Web-сервисов и. NET-классы можно помечать как транзакционные, присваивая им атрибут декларативной транзакции (declarative transaction attribute).

ASP.NET

<@ Page Transaction="Required" > Web-сервис ASP.NET <%@ WebService Language="VB" Class="Class1" %> <%@ assembly name="System.EnterpriseServices" %> … Public Class Class1 Inherits WebService <WebMethod(TransactionOption:= TransactionOption.RequiresNew) > _ Public Function Method1() …

Для участия в автоматических транзакциях. NET-класс должен наследовать от System.EnterpriseServices.ServicedComponent , который обеспечивает выполнение класса в COM+. Если вы сделаете именно так, COM+, взаимодействуя с DTC, создаст распределенную транзакцию и подключит к ней все необходимые ресурсы без вашего участия. Кроме того, вам нужно присвоить классу атрибут декларативной транзакции, чтобы определить его поведение при выполнении транзакции.

Visual Basic .NET

Visual C# .NET

public class Class1: ServicedComponent { … }

Транзакционный атрибут класса принимает одно из следующих значений:

  • Disabled. Указывает, что объект никогда не создается в транзакции COM+. Для поддержки транзакций объект может обращаться к DTC напрямую.
  • NotSupported. Указывает, что объект никогда не создается в транзакции.
  • Supported. Указывает, что объект выполняется в контексте транзакции своего создателя. Если объект сам является корневым или если его создатель не выполняется в транзакции, объект создается вне транзакции.
  • Required. Указывает, что объект выполняется в контексте транзакции своего создателя. Если объект сам является корневым или если его создатель не выполняется в транзакции, при создании такого объекта создается новая транзакция.
  • RequiresNew. Указывает, что объекту нужна транзакция и что при его создании создается новая транзакция.

В следующем коде содержится. NET-класс, настроенный на выполнение в COM+. Кроме того, атрибутам сборки присваиваются значения, необходимые для конфигурирования свойств COM+-приложения.

Visual Basic .NET

Imports System Imports System.Runtime.CompilerServices Imports System.EnterpriseServices Imports System.Reflection " Детали регистрации. " Имя COM+-приложения в том виде, в каком оно присутствует " в каталоге COM+ " Строгое имя (strong name) для сборки (assembly) Public Class Class1 Inherits ServicedComponent Public Sub Example1() … End Sub End Class

Visual C# .NET

using System; using System.Runtime.CompilerServices; using System.EnterpriseServices; using System.Reflection; // Детали регистрации. // Имя COM+-приложения в том виде, в каком оно присутствует // в каталоге COM+ // Строгое имя для сборки public class Class1: ServicedComponent { public void Example1() { … } }

<Assembly: ApplicationName(“Class1”) > указывает имя COM+-приложения, в которое устанавливаются компоненты сборки. <Assembly: ApplicationActivation(ActivationOption.Server) > определяет, является ли это приложение сервером или библиотекой. Когда вы указываете ApplicationActivation(ActivationOption.Server) , сборку необходимо установить в GAC (global assembly cache) с помощью утилиты командной строки gacutil (GacUtil.exe).

Для преобразования сборки в библиотеку типов, регистрации библиотеки типов и ее установки в заданное COM+-приложение можно использовать утилиту командной строки Regsvcs.exe. Кроме того, эта утилита настраивает свойства, добавленные в сборку программным способом. Например, если в сборке указано ApplicationActivation(ActivationOption.Server) , утилита создаст серверное приложение. Если вызванная сборка еще не установлена в COM+, исполняющая среда создаст и зарегистрирует библиотеку типов, а затем установит ее в COM+. COM+-приложение, созданное для сборки, можно просмотреть и настроить в оснастке Component Services.

Процесс создания, регистрации и использования обслуживаемых компонентов (serviced components) подробно рассматривается в разделе Writing Serviced Components (EN) руководства.NET Framework Developer’s Guide.

В следующем коде показывается транзакционный класс, сконфигурированный для запуска под управлением COM+, в котором в рамках транзакции выполняются две SQL-команды. Первая вставляет заголовок заказа в таблицу заказов и возвращает OrderId добавленного заказа. Этот OrderId используется второй командой при вставке позиций заказа в таблицу OrderDetails. Транзакция отменяется, если не удалось выполнить хотя бы одну из двух команд; при этом записи в базу данных не добавляются.

Visual Basic .NET

<Transaction(TransactionOption.Required) > Public Class Class1 Inherits ServicedComponent Public Sub Example1() … Try " Создаем новое соединение conn = New SqlConnection("ConnString") " Открываем соединение conn.Open() " Создаем новый объект Command cmd = New SqlCommand() " Вставляем заголовок заказа " Присваиваем значения свойствам Command With cmd1 .CommandType = CommandType.StoredProcedure .CommandText = "InsertOrderHeader" .Connection = conn " Добавляем входные и выходные параметры.Parameters.Add("@CustomerId", SqlDbType.Int) … .ExecuteNonQuery() " Очищаем параметры для следующей команды .Parameters.clear() End With " Вставляем позиции заказа " Настраиваем свойства Command With cmd .CommandType = CommandType.StoredProcedure .CommandText = "InsertOrderDetail" .Connection = conn " Добавляем параметры.Parameters.Add("@OrderId", SqlDbType.Int) … " Выполняем команду .ExecuteNonQuery() " Повторяем эти строки для каждой позиции заказа End With " Фиксируем транзакцию ContextUtil.SetComplete() Catch " Откатываем транзакцию ContextUtil.SetAbort() Finally " Код очистки End Try End Sub

Используя класс System.EnterpriseServices.ContextUtil , можно получить информацию о контексте COM+-объекта. Этот класс предоставляет методы SetComplete и SetAbort , позволяющие явным образом фиксировать и откатывать транзакцию. Легко догадаться, что метод ContextUtil.SetComplete вызывается в самом конце блока try, когда все операции выполнены успешно и нужно зафиксировать транзакцию. Все исключения перехватывается в блоке catch, где транзакция отменяется с помощью ContextUtil.SetAbort .

Кроме того, с помощью класса-атрибута (attribute class) System.EnterpriseServices.AutoComplete можно добиться, чтобы обслуживаемый компонент автоматически определял, фиксировать или откатывать транзакцию. Компонент «голосует» за фиксацию транзакции, если вызов метода завершился успешно. Если вызов метода привел к генерации исключения, транзакция автоматически отменяется; явный вызов ContextUtil.SetAbort не нужен. Чтобы воспользоваться этой возможностью, вставьте атрибут перед методом класса:

Visual Basic .NET

Public Class Class1 Inherits ServicedComponent Public Sub Example1() … End Sub End Class

Visual C# .NET

public class Class1: ServicedComponent { public void Example1() { … } }

Атрибут <AutoComplete > предлагает самый простой способ программирования транзакций, позволяя обходиться без явных фиксации и отката транзакций. Вы получите точно такую же функциональность, что и в предыдущем примере, где для отмены транзакции явно вызывался метод ContextUtil.SetAbort в блоке catch. Недостаток этого способа в том, что выполнение транзакции неочевидно и при сопровождении кода о ней можно забыть. Кроме того, нет возможности вывода дружественного к пользователю сообщения, если транзакция терпит неудачу. В таких случаях следует явно перехватывать все исключения, вызывать ContextUtil.SetAbort и показывать требуемое сообщение.

В системах, где нужно выполнять транзакции, использующие MSMQ и другие ресурсы, единственно возможный выбор - применение транзакций DTC или COM+. DTC координирует все диспетчеры ресурсов, участвующие в распределенной транзакции, а также управляет деятельностью, связанной с транзакциями. Пример распределенной транзакции MSMQ и SQL Server см. в статье Дункана Мак-Кензи Reliable Messaging with MSMQ and .NET (EN) .

Заключение

При использовании каждой из технологий работы с транзакциями приходится идти на компромис между производительностью приложения и удобством сопровождения кода. Запуск реализованной в хранимой процедуре транзакции базы данных обеспечивает наилучшую производительность, так как требуется лишь один двусторонний обмен информацией с базой данных. Кроме того, обеспечивается гибкость управления транзакциями, поскольку явно указываются начало и завершение транзакции. Но, хотя этот способ дает хорошую производительность и гибкость, приходится программировать на Transact SQL, что не так легко, как на языках.NET.

Транзакции, выполняемые вручную с помощью транзакционных объектов ADO.NET, просты в программировании. Благодаря явному применению инструкций начала и завершения транзакции, возможно гибкое управление границами транзакции. Платой за эту легкость и гибкость является снижение производительности за счет дополнительных двусторонних обменов информацией с базой данных при выполнении транзакции.

Автоматическая транзакция - единственно возможный выбор, когда транзакция использует несколько диспетчеров ресурсов, поддерживающих транзакции, например базы данных SQL Server, очереди сообщений MSMQ и т. д. Они значительно упрощают разработку приложений и предъявляют более низкие требования к квалификации программиста. Однако из-за того, что всю работу по координации выполняет служба COM+, возможны дополнительные издержки.