Содержание
Введение
Транзакция — это последовательность операций, выполняемых как единое
целое. Благодаря объединению взаимосвязанных операций в транзакцию
гарантируется согласованность и целостность данных в системе, несмотря
на любые ошибки, которые могли возникнуть в ходе транзакции. Для
успешного выполнения транзакции необходимо успешное завершение всех
ее операций.
У транзакции есть начало и конец, определяющие ее границы
(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()
' Настраиваем свойство Transaction на транзакцию, где выполняется
' SQL-команда
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)
Reliable Messaging with MSMQ and .NET (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
<Transaction(TransactionOption.Required)> Public Class Class1
Inherits ServicedComponent
Visual C# .NET
[Transaction(TransactionOption.Required)]
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+
<Assembly: ApplicationName("Class1")>
' Строгое имя (strong name) для сборки (assembly)
<Assembly: AssemblyKeyFileAttribute("class1.snk")>
<Assembly: ApplicationActivation(ActivationOption.Server)>
<Transaction(TransactionOption.Required)> 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+
[Assembly: ApplicationName("Class1")]
// Строгое имя для сборки
[Assembly: AssemblyKeyFileAttribute("class1.snk")]
[Assembly: ApplicationActivation(ActivationOption.Server)]
[Transaction(TransactionOption.Required)]
public class Class1 : ServicedComponent {
[AutoComplete]
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 не нужен. Чтобы воспользоваться этой
возможностью, вставьте атрибут <AutoComplete> перед методом класса:
Visual Basic .NET
<Transaction(TransactionOption.Required)> Public Class Class1
Inherits ServicedComponent
<AutoComplete()> Public Sub Example1()
…
End Sub
End Class
Visual C# .NET
[Transaction(TransactionOption.Required)]
public class Class1 : ServicedComponent {
[AutoComplete]
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+, возможны
дополнительные издержки.