Инфраструктура System.Transactions в мире .NET

ctdfaqnuvdjaw6tobyxja78mk-s.png


Встречали ли вы в C# конструкцию типа using (var scope = new TransactionScope(TransactionScopeOption.Required))? Это значит, что код, выполняющийся в блоке using, заключается в транзакцию и после выхода из этого блока изменения будут зафиксированы или отменены. Звучит понятно, пока не начинаешь копать глубже. И чем глубже копаешь, тем «страньше и страньше» становится. Во всяком случае, у меня при более близком знакомстве с классом TransactionScope и вообще транзакциями .NET возникла целая уйма вопросов.

Что за класс TransactionScope? Как только мы используем конструкцию using (var scope = new TransactionScope()), все в нашей программе сразу становится транзакционным? Что такое «управляющий ресурсами» (Resource Manager) и «управляющий транзакциями» (Transaction Manager)? Можно ли написать свой управляющий ресурсами и как он «подключается» к созданному экземпляру TransactionScope? Что такое распределенная транзакция и правда ли, что распределенная транзакция в SQL Server или Oracle Database — это то же самое, что и распределенная транзакция .NET?

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

Содержание


Введение


Что такое транзакции и какие проблемы они решают


Транзакции, о которых здесь идет речь, — это операции, переводящие систему из одного приемлемого состояния в другое и гарантированно не оставляющие систему в неприемлемом состоянии даже при возникновении непредвиденных ситуаций. Что это за приемлемые состояния, в общем случае зависит от контекста. Здесь мы будем считать приемлемой ситуацию, в которой обрабатываемые нами данные целостны. При этом подразумевается, что изменения, составляющие транзакцию, все вместе либо совершаются, либо не совершаются. Кроме того, изменения одной транзакции могут быть изолированы от изменений, вносимых в систему другой транзакцией. Основные требования, предъявляемые к транзакциям, обозначаются аббревиатурой ACID. Для первого знакомства с ними подойдет статья в «Википедии».

4rp72bf2l_qdxmlhawxsjslkj-i.png

Классический пример транзакции — перевод денег между двумя счетами. В этой ситуации снятие денег со счета № 1 без зачисления на счет № 2 неприемлемо, точно так же, как и зачисление на счет № 2 без снятия со счета № 1. Другими словами, мы хотим, чтобы обе операции — и снятие, и зачисление — выполнялись сразу. Если же какую-то из них выполнить не удастся, то и вторая операция выполняться не должна. Можно называть этот принцип «все или ничего». Более того, желательно, чтобы операции выполнялись синхронно даже в случае таких системных сбоев, как отключение электроэнергии, то есть чтобы мы видели систему в приемлемом состоянии, как только она станет доступна после восстановления.

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

Проиллюстрируем пример с переводом денег между двумя счетами. На первой картинке изображена ситуация, когда перевод 50 рублей со счета № 1 на счет № 2 завершился успешно. Зеленый цвет подчеркивает, что система находится в приемлемом состоянии (данные целостны).

snazracqgy0u8fdxtrtksb8jc7g.png

Теперь представим, что перевод осуществляется вне транзакции и после снятия денег со счета № 1 произошел сбой, из-за которого снятые деньги не были начислены на счет № 2. Система окажется в неприемлемом состоянии (красный цвет).

yj8hphqu7cszpcdvnboh97h3xfe.png

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

3d6bwim5vko1yam5mhiv1ekl9ve.png

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

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

Какие транзакции здесь рассматриваются


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

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

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

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

Пользоваться СУБД, конечно, не обязательно. Грубо говоря, можно вообще реализовать механизм транзакций СУБД на своем любимом языке программирования и наслаждаться неустойчивым и заваленным ошибками аналогом существующих инструментов. Зато ваш «велосипед» может быть оптимизирован для конкретных ситуаций предметной области.

Есть и более интересные варианты. Современные промышленные языки программирования (C# и Java в первую очередь) предлагают средства, предназначенные специально для организации транзакций с участием совершенно разных подсистем, а не только СУБД. В данной публикации будем называть такие транзакции программными. В случае C# это транзакции из пространства имен System.Transactions (второй пример), именно о них и рассказывается ниже.

Прежде чем перейти к транзакциям System.Transactions, нельзя не упомянуть еще одно интересное явление. Средства System.Transactions позволяют программисту самостоятельно реализовать программную транзакционную память. В этом случае программные операции, влияющие на состояние системы (в случае классических императивных языков программирования это операция присваивания), по умолчанию включаются в транзакции, которые можно фиксировать и откатывать почти так же, как и транзакции СУБД. При таком подходе необходимость использовать механизмы синхронизации (в C# — lock, в Java — synchronized) значительно снижается. Дальнейшим развитием этой идеи является программная транзакционная память, поддерживаемая на уровне платформы (третий пример). Такое чудо ожидаемо обнаруживается в языке, изящность которого превосходит его промышленную применимость, — Clojure. А для рабоче-крестьянских языков существуют подключаемые библиотеки, предоставляющие функциональность программной транзакционной памяти.

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

diyd4spwelqhd61eoetfacuncjm.png

TL; DR по разделу


Существуют процессы, которые состоят из нескольких неделимых (атомарных) операций, применяемых к системе, в общем случае не обязательно информационной. Каждая неделимая операция может оставить систему в неприемлемом состоянии, когда целостность данных нарушается. Например, если перевод денег между двумя счетами представлен двумя неделимыми операциями снятия со счета № 1 и зачисления на счет № 2, то выполнение только одной из этих операций нарушит целостность данных. Деньги или пропадут неизвестно куда, или появятся неизвестно откуда. Транзакция объединяет неделимые операции так, чтобы они выполнялись все вместе (разумеется, последовательно, если это необходимо) или не выполнялись вообще. Можно говорить о транзакциях предметной области и о транзакциях в технических системах, которые обычно реализуют транзакции предметной области.

Транзакции на основе System.Transactions


Что это


В мире .NET существует программный каркас, предназначенный создателями платформы для управления транзакциями. С точки зрения программиста, пользующегося транзакциями, этот каркас состоит из типов TransactionScope, TransactionScopeOption, TransactionScopeAsyncFlowOption и TransactionOptions пространства имен System.Transactions. Если говорить про .NET Standard, то все это доступно начиная с версии 2.0.

Транзакции из пространства имен System.Transactions основываются на стандарте X/Open XA от консорциума The Open Group. В этом стандарте вводятся многие термины, обсуждаемые далее, и, главное, описываются распределенные транзакции, которым в данной публикации также посвящен специальный раздел. На этом же стандарте основываются реализации программных транзакций и в других платформах, например в Java.

Типовой сценарий использования транзакций для программиста C# следующий:

using (var scope = new System.Transactions.TransactionScope(System.Transactions.TransactionScopeOption.Required))
{
    // Какой-то транзакционный код, например работа с СУБД.
    scope.Complete();
}


Внутри блока using помещается код, выполняющий работу, результаты которой должны быть зафиксированы или отменены все вместе. Классические примеры такой работы — это чтение и запись в БД или отправка и получение сообщений из очереди. Когда управление покинет блок using, транзакция будет зафиксирована. Если убрать вызов Complete, то транзакция откатится. Довольно просто.

Получается, что во время отката транзакции все операции, сделанные внутри такого блока using, будут отменены? И если я присвоил какой-нибудь переменной другое значение, то у этой переменной восстановится старое значение? Когда я впервые увидел подобную конструкцию, то так и подумал. На самом деле, конечно, откатятся не все изменения, а только очень особенные. Если бы откатывались вообще все изменения, то это и было бы программной транзакционной памятью, описанной выше. А сейчас посмотрим, что это за особенные изменения, которые могут участвовать в программных транзакциях на основе System.Transactions.

Управляющие ресурсами


Чтобы нечто поддерживало транзакции на основе System.Transactions, нужно, чтобы оно обладало информацией о том, что в данный момент идет транзакционная работа, и чтобы оно регистрировалось в каком-то реестре участников транзакции. Получить информацию о том, идет ли транзакционная работа, можно, проверив статическое свойство Current класса System.Transactions.Transaction. Вход в блок using указанного выше вида как раз устанавливает это свойство, если оно не было установлено раньше. А чтобы зарегистрироваться как участник транзакции, можно использовать методы типа Transaction.EnlistSmth. Кроме этого, нужно реализовать интерфейс, требуемый данными методами. Управляющий ресурсами (Resource Manager) — это именно такое «нечто», поддерживающее взаимодействие с транзакциями из System.Transactions (более конкретное определение дано ниже).

Какие бывают управляющие ресурсами? Если мы работаем из C# с СУБД, например SQL Server или Oracle Database, то обычно пользуемся соответствующими драйверами, а они и являются управляющими ресурсами. В коде они представляются типами System.Data.SqlClient.SqlConnection и Oracle.ManagedDataAccess.Client.OracleConnection. Еще, говорят, MSMQ поддерживает транзакции на основе System.Transactions. Руководствуясь знаниями и примерами, почерпнутыми из интернета, можно создать и собственный управляющий ресурсами. Простейший пример приведен в следующем разделе.

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

Если говорить конкретнее, то управляющий ресурсами — это экземпляр класса, реализующего специальный интерфейс System.Transactions.IEnlistmentNotification. Экземпляр класса по указанию клиента регистрируется как участник транзакции, пользуясь статическим свойством System.Transactions.Transaction.Current. В дальнейшем управляющий транзакциями по мере необходимости вызывает методы указанного интерфейса.

tweavpimeod539do_fkzpr6yzrw.png

Ясно, что во время выполнения может изменяться набор управляющих ресурсами, участвующих в транзакции. Например, после входа в блок using мы можем сначала сделать что-то в SQL Server, а потом — в Oracle Database. В зависимости от такого набора управляющих ресурсами и определяется используемый управляющий транзакциями. Если быть точнее, то по набору управляющих ресурсами определяется используемый транзакционный протокол, а уже на основе протокола определяется поддерживающий его управляющий транзакциями. Транзакционные протоколы мы рассмотрим позже, когда будем говорить о распределенных транзакциях. Механизм автоматического выбора подходящего управляющего транзакциями во время выполнения при изменении участвующих в транзакции управляющих ресурсами называется продвижением транзакции (Transaction Promotion).

Виды управляющих ресурсами


Управляющие ресурсами можно разделить на две большие группы: долговечные и непостоянные.

Долговечный управляющий ресурсами (Durable Resource Manager) — управляющий ресурсами, который поддерживает транзакцию даже в случае недоступности информационной системы (например, при перезапуске компьютера). Непостоянный управляющий ресурсами (Volatile Resource Manager) — управляющий ресурсами, который не поддерживает транзакцию в случае недоступности информационной системы. Непостоянный управляющий ресурсами поддерживает транзакции только в оперативной памяти.

Классические долговечные управляющие ресурсами — это СУБД (или драйвер СУБД для программной платформы). Что бы ни произошло — хоть сбой в работе операционной системы, хоть отключение электроэнергии, — СУБД будет гарантировать целостность данных после того, как она снова придет в рабочее состояние. За это, разумеется, приходится платить некоторыми неудобствами, но в данной статье мы их рассматривать не будем. Пример непостоянного управляющего ресурсами — упомянутая выше программная транзакционная память.

Использование TransactionScope


При создании объекта типа TransactionScope можно указывать некоторые параметры.

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

1) пользоваться уже существующей к этому моменту транзакцией;

2) обязательно создавать новую;

3) наоборот, выполнять код внутри блока using вне транзакции.

За все это отвечает перечисление System.Transactions.TransactionScopeOption.

Во-вторых, можно задать уровень изоляции транзакции. Это параметр, который позволяет найти компромисс между независимостью изменений и скоростью работы. Самый независимый уровень — сериализуемый — гарантирует, что не существует таких ситуаций, когда изменения, внесенные в рамках одной еще не зафиксированной транзакции, можно увидеть в другой транзакции. Каждый следующий уровень добавляет по одной такой конкретной ситуации, когда одновременно выполняющиеся транзакции могут влиять друг на друга. Настройка уровня изоляции транзакции при создании TransactionScope носит рекомендательный характер для управляющих ресурсами. Они могут даже не поддерживать все уровни, представленные в перечислении System.Transactions.IsolationLevel.

Типовые сценарии работы c TransactionScope и существенные подводные камни (а именно вложенные транзакции) хорошо освещены в этой статье на «Хабре».

Применимость программных транзакций


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

В каждом конкретном случае конструктивно было бы рассматривать стоимость:

1) интеграции процессов в инфраструктуру программных транзакций (нужно, чтобы эти процессы знали о TransactionScope и о многих других вещах);

2) поддержания этой инфраструктуры (например, стоимость аренды оборудования с Windows на борту);

3) обучения сотрудников (так как тема транзакций .NET мало распространена).

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

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

Пример непостоянного управляющего ресурсами


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

internal sealed class Stm : System.Transactions.IEnlistmentNotification {
    private T _current;
    private T _original;

    public T Value {
        get { return _current; }
        set {
            if (!Enlist()) {
                _original = value;
            }
            _current = value;
        }
    }

    public Stm(T value) {
        _current = value;
        _original = value;
    }

    private bool Enlist() {
        var currentTx = System.Transactions.Transaction.Current;
        if (currentTx == null)
            return false;
        currentTx.EnlistVolatile(this, System.Transactions.EnlistmentOptions.None);
        return true;
    }

    #region IEnlistmentNotification

    public void Commit(System.Transactions.Enlistment enlistment) {
        _original = _current;
    }

    public void InDoubt(System.Transactions.Enlistment enlistment) { }

    public void Prepare(System.Transactions.PreparingEnlistment preparingEnlistment) {
        preparingEnlistment.Prepared();
    }

    public void Rollback(System.Transactions.Enlistment enlistment) {
        _current = _original;
    }

    #endregion IEnlistmentNotification
}


Видно, что единственным формальным требованием является реализация интерфейса System.Transactions.IEnlistmentNotification. Из интересного стоит отметить методы Enlist (не являющиеся частью System.Transactions.IEnlistmentNotification) и Prepare. Метод Enlist как раз проверяет, работает ли данный код в рамках транзакции, и, если да, регистрирует экземпляр своего класса в качестве непостоянного управляющего ресурсами. Метод Prepare вызывается управляющим транзакциями перед фиксацией изменений. Наш управляющий ресурсами сигнализирует о своей готовности к фиксации, вызывая метод System.Transactions.PreparingEnlistment.Prepared.

Дальше приведен код, показывающий пример использования нашего управляющего ресурсами:

var stm = new Stm(1);
using (var scope = new System.Transactions.TransactionScope(System.Transactions.TransactionScopeOption.Required)) {
    stm.Value = 2;
    scope.Complete();
}


Если сразу после выхода из блока using прочитать свойство stm.Value, то там ожидаемо будет установленное значение 2. А если убрать вызов scope.Complete, то транзакция откатится и в свойстве stm.Value будет значение 1, заданное до начала транзакции.

Упрощенно последовательность вызовов при работе с транзакциями System.Transactions показана на приведенной ниже диаграмме.

09je1dkghhmmwts6rvaufuntpna.png

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

TL; DR по разделу


Программист может использовать класс TransactionScope для того, чтобы выполнить некоторый код в рамках имеющейся или новой транзакции. Транзакция фиксируется тогда и только тогда, когда у имеющегося экземпляра класса TransactionScope вызывается метод Dispose, при том что перед этим у него был вызван метод Complete. Программист может указывать, желает ли он непременно начать новую транзакцию, воспользоваться уже имеющейся или, наоборот, выполнить код вне имеющейся транзакции. В транзакции участвуют только управляющие ресурсами — программные компоненты, реализующие определенную функциональность. Управляющие ресурсами могут быть долговечными (восстанавливающимися после сбоя системы) и непостоянными (не восстанавливающимися). СУБД — пример долговечного управляющего ресурсами. Координацией управляющих ресурсами занимается управляющий транзакциями — программный компонент, который выбирается исполняющей средой автоматически, без участия программиста.

Непостоянный управляющий ресурсами — это класс, реализующий интерфейс System.Transactions.IEnlistmentNotification и в методе Prepare подтверждающий свою готовность к фиксации изменений или, наоборот, сигнализирующий об откате изменений. Когда вызывающая сторона что-то делает с управляющим ресурсами, он проверяет, открыта ли сейчас транзакция, и, если открыта, регистрируется с помощью метода System.Transactions.Transaction.EnlistVolatile.

Распределенные транзакции


Что это


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

Выше были представлены разные средства реализации транзакций: СУБД, инфраструктура System.Transactions, встроенная в платформу программная транзакционная память. Этими инструментами могут обеспечиваться и распределенные транзакции. Например, в СУБД Oracle Database изменение (а на самом деле и чтение) данных в нескольких БД в рамках одной транзакции автоматически превращает ее в распределенную. Далее же пойдет речь о программных распределенных транзакциях, которые могут включать разнородные управляющие ресурсами.

Транзакционные протоколы


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

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

OleTx. Допускается взаимодействие между несколькими доменами-приложениями и несколькими компьютерами. Допускается использование множества долговечных управляющих ресурсами. Все участвующие компьютеры должны находиться под управлением Windows. Используется удаленный вызов процедуры (Remote Procedure Calls, RPCs).

WS-AT. Допускается взаимодействие между несколькими доменами-приложениями и несколькими компьютерами. Допускается использование множества долговечных управляющих ресурсами. Участвующие компьютеры могут находиться под управлением различных ОС, не только Windows. Используется протокол передачи гипертекста (Hypertext Transmission Protocol, HTTP).

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

Lightweight Transaction Manager (LTM). Представлен в .NET Framework 2.0 и в более поздних версиях. Управляет транзакциями, использующими протокол Lightweight.

Kernel Transaction Manager (KTM). Представлен в Windows Vista и Windows Server 2008. Управляет транзакциями, использующими протокол Lightweight. Может вызывать транзакционную файловую систему (Transactional File System, TxF) и транзакционный реестр (Transactional Registry, TxR) в Windows Vista и Windows 2008.

Distributed Transaction Coordinator (MSDTC). Управляет транзакциями, использующими протоколы OleTx и WS-AT.

Нужно также иметь в виду, что часть управляющих ресурсами не поддерживает все перечисленные протоколы. Например, MSMQ и SQL Server 2000 не поддерживают Lightweight, поэтому транзакции с участием MSMQ или SQL Server 2000 будут управляться MSDTC, даже если они являются единственными участниками. Технически это ограничение возникает от того, что указанные управляющие ресурсами, реализуя, конечно, интерфейс System.Transactions.IEnlistmentNotification, не реализуют интерфейс System.Transactions.IPromotableSinglePhaseNotification. В нем есть, кроме прочего, метод Promote, который исполняющая среда вызывает при необходимости перейти на более «крутой» управляющий транзакциями.

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

1) не менее двух любых управляющих ресурсами;

2) сколько угодно непостоянных управляющих ресурсами и не менее двух долговечных;

3) не менее двух любых управляющих ресурсами, обязательно расположенных на разных компьютерах.

Поэтому лучше всегда уточнять, о каких именно транзакциях идет речь.

И в этом контексте в первую очередь обсуждается MSDTC. Он представляет собой программный компонент Windows, управляющий распределенными транзакциями. Имеется графический интерфейс для конфигурации и наблюдения за транзакциями, который можно обнаружить в утилите «Службы компонентов», пройдя по пути «Компьютеры — Мой компьютер — Координатор распределенных транзакций — Локальная DTC».

vo35gcdta3c9qx5w5cnsuxkbzz8.png

Для конфигурации нужно выбрать пункт «Свойства» в контекстном меню узла «Локальная DTC», а для наблюдения за распределенными транзакциями — пункт «Статистика транзакций» в центральной панели.

Двухфазная фиксация


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

В общем суть этого протокола заключается в следующем. Во время первой фазы участвующие в транзакции управляющие ресурсами подготавливают информацию, достаточную для восстановления после сбоя (если это долговечный управляющий ресурсами) и для успешного окончания работы в результате фиксации. С технической точки зрения управляющий ресурсами сигнализирует о том, что он закончил первую фазу, вызывая метод System.Transactions.PreparingEnlistment.Prepared в методе Prepare. Либо управляющий ресурсами может уведомить об откате изменений, вызвав метод ForceRollback.

Когда все участвующие в транзакции управляющие ресурсами «проголосовали», то есть уведомили управляющий транзакциями о том, хотят ли они фиксировать или откатывать изменения, начинается вторая фаза. В это время управляющие ресурсами получают указание фиксировать свои изменения (если все участники проголосовали за фиксацию) или отказаться от изменений (если хотя бы один участник проголосовал за откат). Технически это выражается в вызове методов Commit и Rollback, которые реализуют управляющие ресурсами и в которых они вызывают метод System.Transactions.Enlistment.Done.

Управляющий ресурсами может вызвать метод System.Transactions.Enlistment.Done и во время первой фазы. В таком случае подразумевается, что он не собирается фиксировать никаких изменений (например, работает только на чтение) и не будет участвовать во второй фазе. Подробнее про двухфазную фиксацию можно прочитать у Microsoft.

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

Шпаргалка по интерфейсам System.Transactions


Что-то сложно получается. Чтобы немного разложить все по полочкам, я кратко опишу основные интерфейсы пространства имен System.Transactions, нужные для создания управляющего ресурсами. Вот диаграмма классов.

bsrjsxptqz6w3g7l6zxawrzeyco.png

IEnlistmentNotification. Управляющий ресурсами обязательно реализует этот интерфейс. Управляющий транзакциями вызывает реализованные методы в следующем порядке. Во время первой фазы он вызывает метод Prepare (если только не сошлись звезды для вызова метода ISinglePhaseNotification.SinglePhaseCommit, о чем написано в следующем пункте). В рамках этого метода управляющий ресурсами сохраняет необходимую для восстановления после сбоя информацию, готовится к окончательной фиксации изменений на своей стороне и голосует за фиксацию или откат изменений. Если наступает вторая фаза, то в зависимости от доступности управляющих ресурсами и от результатов голосования управляющий транзакциями вызывает один из трех методов: Commit, InDoubt, Rollback.

ISinglePhaseNotification. Управляющий ресурсами реализует этот интерфейс, если хочет предоставить управляющему транзакциями возможность оптимизировать выполнение за счет редуцирования второй фазы фиксации. Если управляющий транзакциями видит только один управляющий ресурсами, то на первой фазе фиксации он пытается вызвать у управляющего ресурсами метод SinglePhaseCommit (вместо IEnlistmentNotification.Prepare) и тем самым исключить голосование и переход ко второй фазе. У этого подхода есть достоинства и недостатки, о которых понятнее всего написано у Microsoft здесь.

ITransactionPromoter. Управляющий ресурсами реализует этот интерфейс (только не напрямую, а через интерфейс IPromotableSinglePhaseNotification), если хочет предоставить управляющему транзакциями возможность придерживаться протокола Lightweight даже при удаленном вызове, пока не наступят другие условия, которые потребуют усложнения протокола. Когда потребуется усложнить протокол, будет вызван метод Promote.

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

Еще немного посмотрим на класс System.Transactions.Enlistment и его наследников. Экземпляры этого типа предоставляет управляющий транзакциями, когда вызывает методы интерфейсов, реализуемые управляющим ресурсами.

qibdxcddktegq5h2xnpuaelygj0.png

Enlistment. Управляющий ресурсами может вызвать единственный метод этого типа — Done, — чтобы сигнализировать об успешном выполнении своей части работы.

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

SinglePhaseEnlistment. С помощью экземпляра этого типа управляющий ресурсами может передать информацию управляющему транзакциями о результатах своей работы при использовании упрощенной схемы (однофазной фиксации).

Ограничения программных распределенных транзакций

© Habrahabr.ru