Ten artykuł pochodzi z serii przygotowań do egzaminu 70-503: Windows Communication Foundation.
No to wiemy już jak włączyć transakcje i co trzeba zrobić, zarówno po stronie serwisu jak i po stronie klienta, aby informacje o transakcji były przekazywane w obie strony. Dzisiaj dowiemy się więcej o obsłudze transakcji od strony kodu.
Transakcje otoczenia
W .NET Framework 2.0 w przestrzeni nazw System.Transaction zostały wprowadzone tzw. transakcje otoczenia (ang. Ambient Transactions). Polega to na tym, że transakcja istnieje w aktualnym wątku lub w kontekście obiektu i wszystkie działania w ramach tego wątku lub kontekstu wchodzą w skład tej transakcji (o ile taka transakcja została w ogóle rozpoczęta).
Aby sprawdzić czy mamy dostępną transakcję otoczenia sprawdzamy statyczne pole Current klasy Transaction:
Transaction ambientTransaction = Transaction.Current;
Jeśli nie ma transakcji, Transaction.Current jest równe null.
Jeśli nasz kod zostanie wywołany poprzez zdalnego klienta (przez binding w którym jest włączone przekazywanie transakcji) to ta lokalna transakcja stanie się transakcją rozproszoną (ang. distributed transaction).
Klasa Transaction (i obiekt Transaction.Current) udostępnia właściwość TransactionInformation, z której możemy dowiedzieć się jaka jest “lokalność” transakcji. Właściwość TransactionInformation zawiera dwa pola: LocalIdentifier – string zawierający unikalny identyfikator aktualnej transakcji, oraz DistributedIdentifier – string zawierający globalny unikalny identyfikator transakcji rozproszonej (GUID). Jeśli transakcja nie została podniesiona do rangi rozproszonej transakcji, wartość DistributedIdentifier będzie równa Guid.Empty.
Klasa TransactionScope
Jak sama nazwa wskazuje, klasa TransactionScope definiuje przestrzeń transakcji:
using (TransactionScope ts = new TransactionScope())
{
//
// tutaj aktualizujemy dane w systemie, itp.
//
ts.Complete();
}
W zależności od tego czy nasz kod jest wykonywany w ramach transakcji otoczenia (ang. ambient transaction) to, albo jest tworzona nowa transakcja LTM (jeśli nie ma transakcji otoczenia), albo to co robimy w ramach tej przestrzeni transakcji jest podłączane pod aktywną transakcję otoczenia.
Istotny jest sposób w jaki zakańczana jest transakcja. Transakcja istnieje dopóki istnieje obiekt klasy TransactionScope (dlatego jest ważne jest użycie using, lub jawne zwolnienie obiektu). Jeśli nie zwolnimy sami obiektu transakcji, może on nie być w ogóle zwolniony (czyli będzie aktywny przez całe działanie serwisu – o tym decyduje odśmiecacz (ang. garbage collector)), a nasza transakcja zakończy się niepowodzeniem przez przekroczenie czasu (ang. timeout).
Jeśli przed zwolnieniem obiektu klasy TransactionScope nie zostanie wywołana metoda Complete() całość transakcji zostanie wycofana (ang. rollback). Jeśli metoda Complete() zostanie wywołana, przy zwalnianiu obiektu cała transakcja zostanie zatwierdzona (ang. commit).
Głosowanie w transakcjach
Tak jak zostało powiedziane w poprzedniej lekcji, transakcje LTP charakteryzują się dwufazowym procesem zatwierdzania transakcji. Wywołanie metody Complete() obiektu TransactionScope nie gwarantuje zatwierdzenia transakcji, informuje ono tylko menadżera transakcji o tym, że u nas jest wszystko w porządku.
W przypadku gdy inny uczestnik transakcji zgłosi niepowodzenie, przy wywołaniu u nas metody Complete() dostaniemy wyjątek TransactionAbortedException. Tak więc obsługę transakcji musimy uzupełnić o obsługę tego wyjątku:
try
{
using(TransactionScope ts = new TransactionScope( ))
{
/* Perform updates here */
ts.Complete( );
}
}
catch(TransactionAbortedException e)
{
/* Rollback updates, if necessary */
}
Zagnieżdżanie transakcji
Domyślnie utworzenie nowej transakcji spowoduje podpięcie się pod aktualną transakcję, ewentualnie jeśli takiej nie ma to utworzenie nowej. Jeden z konstruktorów klasy TransactionScope przyjmuje parametr typu TransactionScopeOption, który pozwala nam decydować czy chcemy utworzyć transakcję zagnieżdżoną czy dołączyć się do bieżącej transakcji, Wartości TransactionScopeOption:
- Required – użyta jest transakcja otoczenia, lub stworzona nowa transakcja jeśli transakcja otoczenia nie istnieje, to jest wartość domyślna (tworząc obiekt klasy TransactionScope chcemy fragment kodu objąć transakcją, bez względu na to czy transakcja już istnieje czy nie),
- RequiresNew – zawsze jest tworzona nowa transakcja i jest ona jednocześnie transakcją główną (ang. root transaction) dla wszystkich następnych,
- Suppress – nawet jeśli istnieje transakcja otoczenia, żadne zmiany w tym fragmencie kodu nie zostaną uwzględnione w transakcji, ta opcja służy do wykonania kodu poza transakcją, wszelkie próby kontaktu z kodem działającym w transakcjach (np. wywołanie metody serwisu, która działa w transakcji spowoduje błąd)
Przykład zagnieżdżonych transakcji:
using(TransactionScope ts1 = new TransactionScope())
{
using(TransactionScope ts2 = new TransactionScope())
{
ts2.Complete();
}
ts1.Complete();
}
Aby obie transakcje były zatwierdzone, metoda Complete() musi być wykonana dwukrotnie (raz dla każdego obiektu). Gdyby transakcja ts1 nie została zatwierdzona, żadne zmiany (nawet te zatwierdzone przez ts2) nie zostaną wprowadzone:
using(TransactionScope ts1 = new TransactionScope())
{
using(TransactionScope ts2 = new TransactionScope())
{
ts2.Complete();
}
}
Kolejnym zagadnieniem jest izolacja transakcji. Niektóre konstruktory klasy TransactionScope przyjmują jako parametr strukturę TransactionOptions, w której jedną z właściwości jest IsolationLevel:
- Serializable – najwyższy poziom, inni użytkownicy nie widzą danych modyfikowanych w ramach transakcji, inne procesy nie mogę zmienić lub dodać danych będących w konflikcie z tą transakcją,
- RepeatableRead – inne procesy mogą oglądać dane modyfikowane przez tą transakcję, ale te dane nie mogą być modyfikowane, ale mogą być dodane nowe dane które będą widoczne w transakcji,
- ReadCommitted – inne procesy nie mogą oglądać niezatwierdzonych (ang. uncommited) danych,
- ReadUncommitted – inne procesy mogą odczytywać i modyfikować niezatwierdzone dane,
- Snapshot - dane mogą być odczytywane, przed zatwierdzeniem transakcji sprawdzane jest czy w trakcje transakcji nie zostały zmienione dane (np. przez procesy zewnętrzne) poprzednio wczytane i modyfikowane w ramach transakcji
- Chaos – zmiany z bardziej izolowanych transakcji nie mogą być zastępowane,
- Unspecified – używany jest inny poziom izolacji, którego nie można określić, ustawienie takiej wartości spowoduje wyrzucenie wyjątku
Zagnieżdżone transakcje muszą używać tego samego poziomu izolacji jeśli chcą dołączyć do transakcji otoczenia (ang. ambient transaction), w przeciwnym przypadku wystąpi wyjątek ArgumentException.
Limity czasu transakcji
Możemy ustawić maksymalny czas trwania transakcji. Poniżej konstruktor z ustawionym czasem:
TransactionScope ts = new TransactionScope(TransactionScopeOption.Required,
new TimeSpan(0, 10, 0));
Możemy też ustawić nieskończony czas trwania (co oczywiście jest dość ryzykowne):
TransactionScope ts = new TransactionScope(TransactionScopeOption.Required,
TimeSpan.Zero);
Popatrzmy jeszcze na przykład zagnieżdżonych transakcji:
1: using(TransactionScope ts1 = new
2: TransactionScope(TransactionScopeOption.Required,
3: new TimeSpan(0, 2, 0) ))
4: {
5: // A transaction with a timespan of 2 minutes is created
6: using(TransactionScope ts2 = new
7: TransactionScope(TransactionScopeOption.Required,
8: new TimeSpan(0, 1, 0) ))
9: {
10: // A transaction with a timespan of 1 minute is created
11: Thread.Sleep(90000);
12: }
13: }
Tutaj transakcja ts1 ma timeout 2 minuty (linia 3), a transakcja ts2: 1 minuta (linia 8). W transakcji ts2 występuje operacja (linia 11) trwająca dłużej niż 2 minuty i to transakcja ts2 spowoduje wycofanie całego bloku (włącznie z ts1).
Na koniec jeszcze informacja jak ustawić domyślny timeout:
<system.transactions>
<defaultSettings timeout="00:00:10" />
</system.transactions>
W ostatnich lekcjach tego kursu powiemy odrobinę o współbieżności.