Monday, December 26, 2011

Распределенные транзакции (Distributed Transactions) и их настройка

Каждый программист, работающий с данными, сталкивался с обычными транзакциями той или иной базы данных. Основная задача транзакций – обеспечить consistency данных после завершения операции: изменения либо успешно сохраняются от начала и до конца, либо полностью откатываются, если что-то пошло не так. Даже если вы ни разу не писали в SQL коде ключевые слова BEGIN TRAN, COMMIT TRAN или ROLLBACK TRAN или нечто подобное, это еще не значит, что вы их не использовали. Все ORM, реализующие паттерн unit of work (Entity Framework, NHibernate и др.) объединяют операции по изменению данных в транзакцию перед сохранением.

Транзакции хорошо работают в рамках одной базы данных, но если у вас распределенная система, которая требует сохранения данных в разных базах на разных серверах, а иногда и платформах, то нужна более тяжелая артиллерия – распределенные транзакции (distributed transactions, DT).

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

Truly Understanding .NET Transactions and WCF Implementation

Работа вложенных транзакций отлично расписана здесь:

Understanding nested transaction scopes

Также не забывайте про MSDN:

http://msdn.microsoft.com/en-us/library/w97s6fw4(v=VS.90).aspx

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

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

1) Сконфигурировать WCF для поддержки DT

Распределенные транзакции не поддерживаются в режиме basicHttpBinding, поэтому нам нужно использовать хотя бы на wsHttpBinding, в binding которого нужно прописать атрибут transactionFlow=”true”:

<wshttpbinding>
    <binding name="wsConfig" transactionflow="true">
         <security mode="None" />
    </binding>
</wshttpbinding>

2) Установить специальные атрибуты в интерфейсе сервиса и его методов

Необходимо добавить атрибуты TransactionFlow для метода в контракте и свойства атрибута OperationBehavior TransactionScopeRequired и опционально TransactionAutoComplete в реализации метода:

[ServiceContract] 
public interface IServiceContract 
{ 
    [OperationContract] 
    [TransactionFlow(TransactionFlowOption.Allowed)] 
    string ServiceMethod(string param1, string param2); 
}

public class Service : IServiceContract 
{ 
    [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = true)] 
    public string ServiceMethod(string param1, string param2) 
    { 
      // some server-side operations with database 
    } 
}

Атрибут TransactionFlow принимает несколько опций: Allowed обозначает, что метод сервиса может вызываться как из кода, обернутого в TransactionScope, так и из обычного. Mandatory требует наличия TransactionScope, а NotAllowed (по умолчанию) заставит сервис игнорировать транзакции на клиенте вообще.

3) Создать на стороне клиента транзакцию, внутри которой вызвать метод WCF сервиса

Выглядит это приблизительно так:

public static string DoSomethingWithRemoteCall(string param1, string param2) 
{ 
    string result = null; 

    TransactionOptions options = 
        new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted, Timeout = TimeSpan.FromSeconds(300) }; 
    using (TransactionScope scope = new TransactionScope(TransactionScopeOption.RequiresNew, options)) 
    { 
        // some client-side database actions 

        using (var ecommerceFactory = new ChannelFactory("Staging")) 
        { 
            ecommerceFactory.Open(); 

            var proxy = ecommerceFactory.CreateChannel(); 
            bool success = false; 

            try 
            { 
                result = proxy.ServiceMethod(param1, param2); 
                success = true; 
            } 
            catch (Exception ex) 
            { 
                // log exception 
                return;

            } 
            finally 
            { 
                if (success) 
                    ecommerceFactory.Close(); 
                else 
                    ecommerceFactory.Abort(); 
            } 
        } 

        // other possible client-side database actions
        scope.Complete(); 
    } 

    return result; 
}

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

TransactionOptions позволяет задать некоторые параметры транзакции. В нашем случае это уровень изоляции (значение по умолчанию Serializable не рекомендуется из-за опасности возникновения дедлоков) и таймаут операции (5 минут).

При создании TransactionScope мы указываем параметр TransactionScopeOption.RequiresNew, что не позволит никакому другому коду обернуть наш метод в свою транзакцию. Подробнее вложенные транзакции описаны здесь.

Внутри TransactionScope в случае, если мы действительно хотим закоммитить транзакцию, мы делаем вызов scope.Complete(). Если нам нужно транзакцию откатить (как в случае с catch в примере), мы просто не вызываем Complete(). Вызывать Complete() нужно после всех клиентских операций с базой данных, которые происходят внутри транзакции, иначе у вас случится ошибка, что connection или provider уже закрыт.

Обратите внимание, что в коде сервиса из предыдущего пункта нет никакого намека на TransactionScope, кроме атрибутов TransactionFlow и OperationBehavior. Он там и не нужен, для стандартного сценария атрибутов достаточно. Однако никто вам не мешает создавать свои вложенные транзакции, как с опцией Required (используем родительскую транзакцию), так и с опциями RequiresNew (новая независимая транзакция) и Suppress (код не будет выполнятся в родительской транзакции).

4) Запустить сервис и клиент

И вуаля – все работает. Или не работает? Говорите, полезли странные ошибки выполнения?

Для того, чтобы распределенные транзакции заработали, необходимо сделать еще правильно сконфигурировать клиенты и сервера:

1) Убедиться, что на всех клиентах и серверах (здесь и далее - включая сервера баз данных и веб-сервера) установлена и запущена служба Distribution Transactions Coordinator. Именно эта служба отвечает за координацию ваших распределенных транзакций.

2) Убедиться, что на всех клиентах и серверах включена поддержка распределенных транзакций. Для этого запускаем Control Panel –> Administrative Tools –> Component Services, идем в Computers –> My Computer –> Distributed Transaction Coordinator –> Properties (контекстное меню) и устанавливаем на вкладке Security следующие параметры:

  • Network DTC Access
  • Allow Remote Clients
  • Allow Inbound
  • Allow Outbound

Component_Services

3) Разрешить работу Distributed Transactions Coordinator во всех установленных брандмауэрах, включая Windows Firewall:

Firewall

4) Убедиться, что все ваши клиенты и сервера находятся в одной локальной сети. В большинстве случаев так оно и есть, но есть исключения, и если вы тот самый счастливчик, то вам придется немного попотеть, реализовывая поддержку протокола WS-Atomic Transaction (WS-AT), который упоминается в общей статье. Если вы тот самый счастливчик, которому надо начинать настраивать WS-AT, то вот еще пара полезных статей:

Configuring WS-Atomic Transaction support (MSDN)

Building transactional Web services with WebSphere Application Server and Microsoft .NET using WS-AtomicTransaction

5) Для работы DTC по локальной сети: все машины должны пинговаться по netbios-имени.

6) Важно: Если вы пошли по нашему пути и запустили тестовую конфигурацию на виртуальных машинах, запущенных с одного образа: переустановить DTC. DTC не работает с одинаковыми CID, а переустановка их сбрасывает. Это проблема, с которой мы столкнулись и которую смогли найти лишь запустив утилиту DTCPing.

Больше деталей здесь: Warning: the CID values for both test machines are the same

7) Если ничего не помогло: поставить и запустить DTCPing и посмотреть, что она говорит. Очень хороший способ, когда ничего другое не помогает:

Troubleshooting MSDTC issues with the DTCPing tool

3 comments:

  1. В дополнение к пункту 6: Есть более общий способ перегенерировать CID'ы: http://en.wikipedia.org/wiki/Sysprep
    Его всегда рекоммендуют делать при клонировании образов ОС

    ReplyDelete
  2. Спасибо за отличную статью!
    С распределенными транзакциями еще не приходилось сталкиваться, и для ознакомления - очень полезная информация.

    ReplyDelete
  3. Статья по первой ссылке удалена c CodeProject, но материал можно найти здесь https://sites.google.com/site/stevenattw/dot-net/truly-understanding-net-transactions-and-wcf-implementation

    ReplyDelete