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.

Stwórz darmową stronę albo bloga na WordPress.com.