sonyps4.ru

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

Класс 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 в одном из конструкторов. Дополнительные сведения см. в разделе

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

Возникает вопрос - зачем нужны транзакции? Представим себе, что в базу данных BDTur_firm2 требуется вставить связанные записи в две таблицы - «Туристы» и «Информация о туристах». Если запись, вставляемая в таблицу «Туристы», окажется неверной, например, из-за неправильно указанного кода туриста, база данных не позволит внести изменения, но при этом в таблице «Информация о туристах» появится ненужная запись. Рассмотрим такую ситуацию на примере.

Запустим Management Studio, в новом бланке введем запрос для добавления двух записей:




VALUES (8, "CA 1234567", "Новосибирск", "Россия", 1234567, 996548);

Две записи успешно добавляются в базу данных:

(1 row(s) affected) //или (строк обработано: 1)
(1 row(s) affected) //или (строк обработано: 1)

Теперь спровоцируем ошибку - изменим код туриста только во втором запросе:

INSERT INTO Туристы ([Код туриста], Фамилия, Имя, Отчество)
VALUES (8, "Тихомиров", "Андрей", "Борисович");
INSERT INTO [Информация о туристах]([Код туриста], [Серия паспорта], Город, Страна, Телефон, Индекс)
VALUES (9, "CA 1234567", "Новосибирск", "Россия", 1234567, 996548);

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

Сообщение 2627, уровень 14, состояние 1, строка 1

The statement has been terminated.
(строк обработано: 1)

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

SELECT * FROM Туристы
SELECT * FROM [Информация о туристах]

В таблице «Информация о туристах» последняя запись добавилась безо всякой связи с записью таблицы «Туристы» (рис. 99). Для того чтобы избегать подобных ошибок, нужно применить транзакцию.

Рис. 99. Содержимое таблиц «Туристы» и «Информация о туристах» - нарушение связи

Удалим все внесенные записи из обеих таблиц и оформим исходные SQL-конструкции в виде транзакции:

BEGIN TRAN

INSERT INTO Туристы ([Код туриста], Фамилия, Имя, Отчество)
VALUES (8, "Тихомиров", "Андрей", "Борисович");


Город, Страна, Телефон, Индекс)
VALUES (8, "CA 1234567", "Новосибирск", "Россия", 1234567, 996548); SELECT @OshibkiTabliciInfoTourists=@@ERROR IF @OshibkiTabliciTourists=0 AND @OshibkiTabliciInfoTourists=0 COMMIT TRAN ELSE
ROLLBACK TRAN

Начало транзакции объявляется с помощью команды BEGIN TRAN. Далее создаются два параметра - @OshibkiTabliciTourists, OshibkiTabliciInfoTourists для сбора ошибок. После первого запроса возвращаем значение, которое встроенная функция @@ERROR присваивает первому параметру:

SELECT @OshibkiTabliciTourists=@@ERROR

То же самое делаем после второго запроса для другого параметра:

SELECT @OshibkiTabliciInfoTourists=@@ERROR

Проверяем значения обоих параметров, которые должны быть равными нулю при отсутствии ошибок:

IF @OshibkiTabliciTourists=0 AND @OshibkiTabliciInfoTourists=0

В этом случае подтверждаем транзакцию (в данном случае внесение изменений) при помощи команды COMMIT TRAN. В противном случае - если значение хотя бы одного из параметров @OshibkiTabliciTourists и @OshibkiTabliciInfoTourists оказывается отличным от нуля, отменяем транзакцию при помощи команды ROLLBACK TRAN.

После выполнения транзакции появляется сообщение о добавлении двух строк:

(строк обработано: 1)
(строк обработано: 1)

Снова изменим код туриста во втором запросе:

BEGIN TRAN
DECLARE @OshibkiTabliciTourists int, @OshibkiTabliciInfoTourists int
INSERT INTO Туристы ([Код туриста], Фамилия, Имя, Отчество)
VALUES (8, "Тихомиров", "Андрей", "Борисович");
SELECT @OshibkiTabliciTourists=@@ERROR
INSERT INTO [Информация о туристах]([Код туриста], [Серия паспорта],
Город, Страна, Телефон, Индекс)
VALUES (9, "CA 1234567", "Новосибирск", "Россия", 1234567, 996548);
SELECT @OshibkiTabliciInfoTourists=@@ERROR
IF @OshibkiTabliciTourists=0 AND @OshibkiTabliciInfoTourists=0
COMMIT TRAN
ELSE
ROLLBACK TRAN

Запускаем транзакцию - появляется в точности такое же сообщение, что и в случае применения обычных запросов:

Сообщение 2627, уровень 14, состояние 1, строка 3
Violation of PRIMARY KEY constraint ‘PK_Туристы". Cannot insert duplicate key in object ‘dbo.Туристы".
The statement has been terminated. (строк обработано: 1)

Однако теперь изменения не были внесены во вторую таблицу (рис. 100).

Рис. 100. Содержимое таблиц «Туристы» и «Информация о туристах» после выполнения неудачной транзакции

Сообщение (1 row(s) affected), указывающее на «добавление» одной записи, в данном случае оно всего лишь означает, что вторая SQL-конструкция была верной, и запись могла быть добавлена в случае успешного выполнения транзакции.

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

Транзакции в ADO . NET

Перейдем теперь к рассмотрению транзакций в ADO .NET. Создадим новое консольное приложение Easy Transaction. Поставим задачу: передать те же самые данные в две таблицы - «Туристы» и «Информация о туристах».

Полный листинг данного приложения выглядит следующим образом:

using System;
using System.Data.SqlClient;
namespace EasyTransaction {
class Class1 {
static void Main(string args)
{
//Создаем соединение
SqlConnection conn = new SqlConnection();
conn.ConnectionString = @"Data Source=.\SQLEXPRESS;AttachDbFilename=" +
@"D:\ВМИ\For ADO\BDTur_firmSQL2.mdf" +
"integrated Security=True;Connect Timeout=30;User Instance=True";
conn.Open();
SqlCommand myCommand = conn.CreateCommand();
//Создаем транзакцию
myCommand.Transaction = conn.BeginTransaction(System.Data.IsolationLevel.Serializable);
try {
myCommand.CommandText =
"INSERT INTO Туристы ([Код туриста], Фамилия, Имя, Отчество) " +
"VALUES (9, "Тихомиров", "Андрей", "Борисович")";
myCommand.CommandText = "INSERT INTO [Информация о туристах]" +
" ([Код туриста], [Серия паспорта], Город, Страна, Телефон, Индекс) " +
"VALUES (9, "CA 1234567", "Новосибирск", "Россия", 1234567, 996548)";
myCommand.ExecuteNonQuery();
//Подтверждаем транзакцию
Console.WriteLine("Передача данных успешно завершена");
}
catch(Exception ex) {
//Отклоняем транзакцию
Console.WriteLine("При передаче данных произошла ошибка: "+ ex.Message);
}
finally {
conn.Close();
}
} end Main
} end Class
} end namespace

Перед запуском приложения снова удаляем все добавленные записи из таблиц. При успешном выполнении запроса появляется соответствующее сообщение, а в таблицы добавляются записи (рис. 101).

Рис. 101. Приложение EasyTransaction. Транзакция выполнена

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

Рис. 102. Приложение EasyTransaction. Транзакция отклонена

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

//Создаем соединение
... см. в примере приложения EasyTransaction
//Создаем транзакцию
myCommand.Transaction = conn.BeginTransaction();
try {
//Выполняем команды, вызываем одну или несколько хранимых процедур
//Подтверждаем транзакцию
myCommand.Transaction.Commit();
}
catch(Exception ex) {
//Отклоняем транзакцию
myCommand.Transaction.Rollback();
}
finally {
//Закрываем соединение
conn.Close();
}

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

1. Dirty reads - «грязное» чтение. Первый пользователь начинает транзакцию, изменяющую данные. В это время другой пользователь (или создаваемая им транзакция) извлекает частично измененные данные, которые не являются корректными.

2. Non-repeatable reads - неповторяемое чтение. Первый пользователь начинает транзакцию, изменяющую данные. В это время другой пользователь начинает и завершает другую транзакцию. Первый пользователь при повторном чтении данных (например, если в его транзакцию входит несколько инструкций SELECT) получает другой набор записей.

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

Для решения этих проблем разработаны четыре уровня изоляции транзакции:

1. Read uncommitted. Транзакция может считывать данные, с которыми работают другие транзакции. Применение этого уровня изоляции может привести ко всем перечисленным проблемам.

2. Read committed. Транзакция не может считывать данные, с которыми работают другие транзакции. Применение этого уровня изоляции исключает проблему «грязного» чтения.

3. Repeatable read. Транзакция не может считывать данные, с которыми работают другие транзакции. Другие транзакции также не могут считывать данные, с которыми работает эта транзакция. Применение этого уровня изоляции исключает все проблемы, кроме чтения фантомов.

4. Serializable. Транзакция полностью изолирована от других транзакций. Применение этого уровня изоляции полностью исключает все проблемы.

По умолчанию установлен уровень Read committed. В справке Microsoft SQL Server 2005 приводится таблица, иллюстрирующая различные уровни изоляции (рис. 103).

Рис. 103. Уровни изоляции Microsoft SQL Server 2005

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

Для установки уровня изоляции применяется следующая команда:

SET TRANSACTION ISOLATION LEVEL
READ UNCOMMITTED
или READ COMMITTED
или REPEATABLE READ
или SERIALIZABLE

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

BEGIN TRAN SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
DECLARE @OshibkiTabliciTourists int, @OshibkiTabliciInfoTourists int
...
ROLLBACK TRAN

В ADO .NET уровень изоляции можно установить при создании транзакции:

myCommand.Transaction = conn.BeginTransaction(System.Data.IsolationLevel.Serializable); Дополнительно поддерживаются еще два уровня (рис. 104):

1. Chaos. Транзакция не может перезаписать другие непринятые транзакции с большим уровнем изоляции, но может перезаписать изменения, внесенные без использования транзакций. Данные, с которыми работает текущая транзакция, не блокируются;

2. Unspecified. Отдельный уровень изоляции, который может применяться, но не может быть определен. Транзакция с этим уровнем может применяться для задания собственного уровня изоляции.

Рис. 104. Определение уровня транзакции

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

Локальные транзакции 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

Давным-давно вышел ADO.NET 2.0, а вместе с ним и сборка System.Transactions, содержащая класс TransactionScope - путеводитель в мир легкого и непринужденного использования транзакций. В сегодняшней статье я рассмотрю некоторые нюансы, возникающие при использовании этой дырявой, но такой симпатичной абстракции.

Итак, начиная с ADO.NET 2.0, для того чтобы заключить свой код в транзакцию, разработчику достаточно расположить его внутри блока TransactionScope:

Using (var transactionScope = new TransactionScope(TransactionScopeOption.Suppress, new TransactionOptions() { IsolationLevel = IsolationLevel.Serializable }) { //код внутри транзакции transactionScope.Complete(); }

Я использовал в конструкторе наиболее важные параметры - давайте их рассмотрим (в обратном порядке).

IsolationLevel

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

Public void void IntegrationTest() { using (new TransactionScope()) { //код теста //не вызываем Complete } }

Если в тестируемом коде создаются TransactionScope в режиме Requires, то они подцепятся к тестовому TransactionScope, а значит мы сможем откатить все изменения. Если же в коде есть TransactionScope в режиме RequiresNew или Suppress, то откатить результат их работы из тестового TransactionScope мы не сможем. Стоит отметить, что наличие логики, завязанной на момент коммита транзакции (как в предыдущем примере), тоже делает невозможным использование этого приема.

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

Транзакции

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

Например, рассмотрим транзакцию, которая передает $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() с точкой сохранения в качестве параметра. Если вы хотите откатить всю транзакцию, просто пропустите этот параметр. При откате транзакции до точки сохранения все точки сохранения, определенные после данной, теряются. Если они нужны, их придется установить заново.



Загрузка...