Projektowanie, Programowanie, Codzienność – BeniaminZaborski.com

5 lutego 2014

Transakcje rozproszone – Wszystko albo nic

Dzisiaj trochę o transakcjach. Wiemy, że transakcja to niepodzielna operacja wykonana na jakimś zasobie lub zasobach. Wiemy, także że transakcję charakteryzują cztery litery (nie, to nie te :)), mianowicie ACID. W języku polskim oznaczają one odpowiednio: Atomowość, Spójność, Izolacja, Trwałość.

Większości z nas pojęcie transakcji kojarzy się przeważnie z bazami danych … właśnie, bazy danych. Celowo na początku napisałem, że transakcja to niepodzielna operacja na jakimś zasobie lub zasobach. Tym zasobem może być baza danych, ale także zupełnie coś innego, np. system plików, serwis WCF, obiekt COM+, itd.

Interesuje mnie aspekt transakcyjności w kontekście liczby zasobów większej niż jeden. Powiedzmy, że moja niepodzielna operacja powinna składać się z: dodania rekordu do tabeli bazy danych i utworzenia pliku w systemie plików. Oznacza to że jeśli któraś z tych dwóch operacji nie powiedzie się całość zostanie wycofana w myśl zasady „wszystko albo nic”. Jeśli kodujemy w .NET przed wersją 2.0, to rzeczywiście możemy mieć lekki problem. Na szczęście w wersji 2.0 frameworka .NET pojawił się nowy mechanizm obsługi transakcji zwany TransactionScope.

Zauważyłem, że TransactionScope jest bardzo powszechnie używany przez programistów .NET. No i nie ma w tym nic złego, ale niestety jest on używany bardzo nieświadomie. Standardowe użycie zna chyba każdy:

using (TransactionScope transaction = new TransactionScope())
{
  SaveDataToDB();
  transaction.Complete();
}

I co tu się dzieje? A no transakcja się dzieje!

Metoda SaveDataToDB zawiera operację zapisu danych do jednej bazy danych. Wszystko działa, ale musimy mieć świadomość, że zadziała się magia i TransactionScope użył tu tzw. Lightweight Transaction Manager (LTM). Praktycznie można powiedzieć, że nasza transakcja przełożyła się bezpośrednio na transakcję bazodanową. Pod kątem wydajności LTM jest zbliżona do natywnej transakcji ADO.NET/bazy danych.

Rozważmy teraz przykład który przytoczyłem na początku, tj.:

using (TransactionScope transaction = new TransactionScope())
{  
  SaveDataToDB();
  CreateFile();
  transaction.Complete();
}

Teraz transakcja obejmuje dwa różne zasoby i znów zadzieje się magia i TransactionScope użyje rozproszonego menadżera transakcji opartego na MSDTC. Trochę teraz skłamałem, gdyż standardowo tworząc plik w .NET np. poprzez File.WriteAllText nie jest obsługiwana transakcyjność. Rozwiązaniem jest zastosowanie zewnętrznej biblioteki do transakcyjnego dostępu do systemu plików NTFS (np. Transactional NTFS Managed Wrapper) lub obsłużenie jej samemu. Obsługa jest dość prosta i wymaga zaimplementowania interfejsu IEnlistmentNotification. Trywialny przykład obsługi mógłby wyglądać tak:

public class TransactionalFileCreator : IEnlistmentNotification
{
  private string path;
  private string content;
  public TransactionalFileCreator(string path, string content)
  {
   this.path = path;
   this.content = content;
   Transaction.Current.EnlistDurable(Guid.NewGuid(), this, EnlistmentOptions.None);
  }
  public void Commit(Enlistment enlistment)
  {
    File.WriteAllText(path, content);
  }
  public void InDoubt(Enlistment enlistment)
  {
    enlistment.Done();
  }
  public void Prepare(PreparingEnlistment preparingEnlistment)
  {
    preparingEnlistment.Prepared();
  }
  public void Rollback(Enlistment enlistment)
  {
    if (File.Exists(path))
      File.Delete(path);
  }
}

Bardzo ważna jest, na powyższym listingu, linia Transaction.Current.EnlistDurable(Guid.NewGuid(), this, EnlistmentOptions.None). To ona jest warunkiem promowania wstępnie zarządzanej transakcji przez LTM do DTC. Według dokumentacji jeśli w ramach transakcji użyjemy co najmniej dwóch zasobów „durable” tj. takich obsługujących promowanie transackji do DTC to takie promowanie się odbędzie. Koszt transakcji zarządzanej przez DTC jest oczywiście znacznie większy niż tej opartej na LTM.

Uruchomienie w transakcji promowanej do DTC naszego kodu wyglądałoby teraz tak:

using (TransactionScope transaction = new TransactionScope())
{  
  SaveDataToDB();
  TransactionalFileCreator fileCreator = new TransactionalFileCreator(path, content);
  var dtIdentifier = Transaction.Current.TransactionInformation.DistributedIdentifier;
  transaction.Complete();
}

Transaction.Current.TransactionInformation.DistributedIdentifier to pobranie identyfikatora transakcji z rozproszonego koordynatora transakcji MSDTC. Naszą transakcję możemy także podejrzeć w „Koordynatorze transakcji rozproszonych” w „Panel sterowania” -> „Usługi składowe” -> „Lokalna usługa DTC” -> „Lista transakcji”:

DTC_tran

Zachęcam do zgłębienia wiedzy na temat TransactionScope, a można zacząć np. tutaj. Zdobycie wiedzy o tym jaki mechanizm zarządza naszą transakcją może nas uratować od niezłych kłopotów.

2 Komentarze »

  1. Przetestowałem Twój kod i nie chciał mi zadziałać. Zrobiłem zapis do dwóch plików, przy czym ścieżka do drugiego nie istnieje i to powinno powodować usunięcie pierwszego pliku. Musiałem przenieść zapis do osobnej metody “DoWork” i ustawić Transaction.Current.EnlistVolatile(this, EnlistmentOptions.None). Zamieszczam kod całego rozwiązania:
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Transactions;
    using System.IO;
    namespace Samples
    {
    class SamplesTransaction : Samples
    {
    public SamplesTransaction()
    {
    }
    public SamplesTransaction(bool isRun) : base(isRun)
    {
    }
    public override void Init()
    {
    ProgramMessages.showMainMessage(this);
    //— https://beniaminzaborski.wordpress.com/2014/02/05/transakcje-rozproszone-wszystko-albo-nic/
    //— http://go4answers.webhost4life.com/Example/tutorial-ienlistmentnotification-6499.aspx
    //— http://msdn.microsoft.com/en-us/library/system.transactions.ienlistmentnotification(v=vs.110).aspx
    try
    {
    using (TransactionScope transaction = new TransactionScope())
    {
    //Enlist on the current transaction with the enlistment object
    TransactionalFileCreator fileCreator = new TransactionalFileCreator(@”c:\tmp.txt”, “ala ma kota”);
    fileCreator.DoWork();
    TransactionalFileCreator fileCreator2 = new TransactionalFileCreator(@”c:\a\tmp1.txt”, “tomek ma psa”);
    fileCreator2.DoWork();
    //var dtIdentifier = Transaction.Current.TransactionInformation.DistributedIdentifier;
    transaction.Complete();
    }
    }
    catch (System.Transactions.TransactionException ex)
    {
    Console.WriteLine(ex);
    }
    catch(Exception ex)
    {
    Console.WriteLine(“Cannot complete transaction: ” + ex.Message);
    }
    }
    }
    public class TransactionalFileCreator : IEnlistmentNotification
    {
    private string path;
    private string content;
    public TransactionalFileCreator(string path, string content)
    {
    this.path = path;
    this.content = content;
    //Transaction.Current.EnlistDurable(Guid.NewGuid(), this, EnlistmentOptions.None); // default
    Transaction.Current.EnlistVolatile(this, EnlistmentOptions.None); // z tym ustawieniem zaczelo dzialac
    }
    public void DoWork()
    {
    File.WriteAllText(path, content);
    }
    public void Commit(Enlistment enlistment)
    {
    Console.WriteLine(“Commit”);
    enlistment.Done();
    }
    public void InDoubt(Enlistment enlistment)
    {
    enlistment.Done();
    }
    public void Prepare(PreparingEnlistment preparingEnlistment)
    {
    preparingEnlistment.Prepared();
    }
    public void Rollback(Enlistment enlistment)
    {
    Console.WriteLine(“Usuwam: ” + path);
    if (File.Exists(path))
    {
    File.Delete(path);
    }
    enlistment.Done();
    }
    }
    }

    Komentarz - autor: lokum09 — 6 lutego 2014 @ 11:22

    • Dzięki za komentarz, ale to co zrobiłeś tj. użycie EnlistVolatile zamiast EnlistDurable spowodowało, że TransactionScope użył LTM jako zarządcy transakcji a nie usługi DTC. A to zarządzanie transakcjami przez DTC było powodem do napisania tego posta przeze mnie. Skoro nie chciało zadziałać na Twoim systemie z EnlistDurable to podejrzewam że jest jakiś problem z usługą MSDTC. Pełny komunikat błędu pewnie by przybliżył nas do rozwiązania.

      Komentarz - autor: Biz — 6 lutego 2014 @ 14:00


RSS feed for comments on this post. TrackBack URI

Dodaj odpowiedź do lokum09 Anuluj pisanie odpowiedzi

Blog na WordPress.com.