TransactionScope — заманчивый, но коварный

    Давным-давно вышел 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


    Старый-добрый IsolationLevel. Enum IsolationLevel включает целых 7 уровней изоляции, но не обольщайтесь — эти значения трактуются лишь как рекомендации ADO.NET провайдеру, а использовать можно лишь те уровни, который поддерживаются вашей СУБД.

    По умолчанию используется самый высокий уровень изоляции — Serializable, и мне даже попадалась критика на этот счет: мол, не по-пацански по стандарту это (в стандарте в качестве дефолтного рекомендуется использовать Read Committed). Мне же это решение наоборот по душе: по умолчанию используется самый надежный режим, а при необходимости улучшить производительность или побороть deadlock'и — всегда можно перейти на более мягкий режим.

    Кстати, менять Isolation Level в ходе транзакции нельзя.

    TransactionScopeOption


    Enum TransactionScopeOption содержит три значения: Requires, RequiresNew, Suppress, которые определяют поведение при входе в блок TransactionScope. Поведение при всех возможных случаях TransactionScopeOption отлично описано в msdn, а я лишь обобщу:
    1. Requires (значение по умолчанию) требует транзакции. При входе в блок будет либо использована транзакция родительского TransactionScope (если он есть), либо создана новая транзакция.
    2. RequiresNew всегда требует создания новой транзакции
    3. Suppress выполняет код блока вне транзакции


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

    public void Method1()
    {
       using (var transactionScope1 = new TransactionScope(TransactionScopeOption.Requires))
       {
             Method2();
             transactionScope1.Complete();
       }
    }
    
    public void Method2()
    {
       using (var transactionScope2 = new TransactionScope(TransactionScopeOption.Requires))
       {
          //some code
       }
    }
    


    Обратите внимание на то, что в Method2 мы не вызвали transactionScope2.Complete, а значит transactionScope2 откатится. В случае классических вложенных транзакций мы можем откатить внутреннюю транзакцию без отката рутовой. Здесь же оба TransactionScope работают в рамках одной транзакции, а значит если хотя бы один из внутренних transactionScope не вызовет Complete, транзакция будет помечена для отката, а уже при выходе из рутового TransactionScope произойдет rollback (commit/rollback транзакции всегда происходит при выходе из рутового TransactionScope). Причем если в рутовом TransactionScope вы попытаетесь вызвать Complete (как в Method1), а транзакция уже была помечена для отката, будет выкинут TransactionAbortedException.

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

    public void TransactionMethod(TransactionScopeOption.Requires)
    {
       using (var transactionScope = new TransactionScope(TransactionScopeOptions.Requires))
       {
          ...
          transactionScope.Complete();
       }
       
       //логика, завязанная на то, что транзакция уже закоммичена
    }
    


    , а также метод, который его вызывает:

    public void CallingMethod1()
    {
       //...
       TransactionMethod();
       //...
    }
    


    И все бы ничего, но со временем появляется более высокоуровневый сервис, который вызывает TransactionMethod уже из своего внутреннего TransactionScope:

    public void CallingMethod1()
    {
       //...
       using (var transactionScope = new TransactionScope(TransactionScopeOptions.Requires))
       {
          //...
          TransactionMethod();
          //...
          transactionScope.Complete();
       }
       //...
    }
    


    И тут вызов transactionScope.Complete() внутри TransactionMethod уже не ведет к коммиту транзакции, а значит и нижележащая логика, завязанная на то, что коммит транзакции уже произошел, даст сбой.

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

    Теперь настало время уделить внимание двум другим значением TransactionScopeOption: RequiresNew и Suppress. Мне крайне редко приходилось использовать эти режимы. Более того, если не ошибаюсь, делал я это лишь один раз, и как раз при решении проблемы, описанной в предыдущей статье.

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

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


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

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

    Вот, пожалуй, и все. TransactionScope прекрасен, но коварен — не забывайте об этом :)
    • +26
    • 19,2k
    • 9
    Поделиться публикацией

    Похожие публикации

    Комментарии 9

      +1
      Что, реально, ни одного комента?:)

      Это все потому, что вы про такой мощный инструмент так просто пишете.

      Really. Поботайте Microsoft Distributed Transactions service, разрюхайте распределенные архитектуры и распишите использование transaction scope уже в этом контексте.

      Вряд ли интересно юзать System.Transactions в рамках одного процесса (гораздо проще IDbTransaction заюзать). В то же время спросите кого угодно про транзакции в сложных архитектурах, и послушайте ответы ;) Возможно, они сподвигнут вас написать еще одну статью, на этот раз с коментами и всеобщим почитанием;)
        +1
        Вряд ли интересно юзать System.Transactions в рамках одного процесса (гораздо проще IDbTransaction заюзать
        Вот здесь поподробней, пожалуйста. Разве System.Transactions вообще можно использовать не из одного процесса? TransactionScope имеет смысл использовать из одного процесса по нескольким причинам:
        1. Доступ в рамках одной транзакции к нескольким источникам данных
        2. В этом пункте не уверен — поправьте, если что. IDbTransaction специфичен по отношению к соединению, а значит я вынужден держать соединение открытым на протяжении всей транзакции. В случае TransactionScope за счет использования распределенной транзакции (которая может использовать несколько соединений) код может выполняться в следующем стиле: открыли соединение — запрос — ответ — закрыли соединение, продолжительные операции, открыли соединение — запрос — ответ — закрыли соединение.
        3. TransactionScope более декларативен, и его использование в слое сервисов выглядит более естественным. Ну это уже субъективный плюс :)

        Возможно, они сподвигнут вас написать еще одну статью, на этот раз с коментами и всеобщим почитанием;)
        На самом деле отсутствие комментариев связано лишь с тем, что этот пост не взывает к какой-либо дискуссии. Вот предыдущий мой пост был дискуссионным, а этот — нет.
          0
          1) ок
          2) профайлер вам в помощь;)
          3) хз. имхо хрень

          про дискуссию — ну вот написали вы статью, получили свои плюсики. с тем же успехом можно было дать ссылку на мсдн.

          про распределенные архитектуры — ботайте MSDTC, MSMQ, книжки по архитектуре.
            0
            хз. имхо хрень
            В первый раз вижу программиста, который уместное применение декларативного подхода называет «хренью». Как, например, насчет того, что декларативный подход позволяет в две строчки заставить существующий код поддерживать транзакции?

            про дискуссию — ну вот написали вы статью, получили свои плюсики. с тем же успехом можно было дать ссылку на мсдн.
            Какую ссылку? Если вам показалось, что эта статья — перепечатка MSDN'а, — вы сильно ошибаетесь

            про распределенные архитектуры — ботайте MSDTC, MSMQ, книжки по архитектуре.
            От всей души желаю, чтобы вам всегда отвечали в таком же духе.
            В целом, да, Transaction можно передавать через AppDomain, но ни один пункт статьи от этого не теряет своей актуальности.
              0
              вместо того, чтобы немного напрячь свой мозг и эрудицию, вы хотите поругаться. я ругаться не хочу и не буду, так как сильно занят.

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

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

              ну и все, куча архитектур подобным образом построена. бери любую, описывай, получай карму и плюсики.
                0
                Вы явно на другой волне. Статья была о нюансах использования TransactionScope. Ни о распределенных архитектурах, ни о MSMQ, ни о чем другом. Если вы соберетесь использовать TransactionScope из нескольких AppDomain, то увидите все те же описанные в статье нюансы. Если я захочу написать статью о распределенных архитектурах, вы первый об этом узнаете.
                  0
                  буду ждать с нетерпением :D
        0
        в стандарте в качестве дефолтного рекомендуется использоВАть Read Committed


          0
          Спасибо, fixed

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

        Самое читаемое