NHibernate, NServiceBus i transakcje
Dziś chciałbym podzielić się z Wami moimi refleksjami na temat sposobu zarządzania transakcjami w NHibernate, ze szczególnym uwzględnieniem nietrywialnego przypadku, kiedy w ramach jednej transakcji wykorzystujemy zarówno NHibernate, jak i NServiceBus. Posłużę się w tym celu kodem DDDSample.Net.
Aby wprowadzić Was w klimat, przypomnę jak wygląda architektura DDDSample. Począwszy od najwyższego poziomu, występują tam następujące warstwy:
- WebUI (ASP.NET MVC) – prezentacja danych, interfejs
- Application – stanowi fasadę dla modelu domeny udostępniając interfejs poszczególnych akcji/komend
- Domain – logika biznesowa żyje tutaj
- Domain.Persistence.NHibernate – mapowania NHibernate oraz implementacje repozytoriów
Która warstwa odpowiada zatem za transakcje? Oczywiście Application. Dlaczego? Ponieważ jednostką izolacji są akcje/komendy — każda taka jednostka wykonywana jest w ramach osobnej transakcji.
Ponieważ transakcyjność postrzegam (zwykle) jako jedno z wymagań niefunkcjonalnych, implementuje ją za pomocą aspektów. Korzystam przy tym z możliwości mojego ulubionego kontenera Unity, jednak to samo można zrobić za pomocą niemal dowolnego innego kontenera. Moja warstwa Application składa się par (interfejs, klasa), przy czym interfejs zdefiniowany jest nie po to, aby umożliwić zmianę implementacji, ale tylko i wyłącznie po to, aby umożliwić AOP (tak, wiem, że można to samo osiągnąć za pomocą metod wirtualnych)
public interface IBookingService
{
TrackingId BookNewCargo(UnLocode origin, UnLocode destination, DateTime arrivalDeadline);
//...
public class BookingService : IBookingService
{
public TrackingId BookNewCargo(UnLocode originUnLocode, UnLocode destinationUnLocode, DateTime arrivalDeadline)
{
Location origin = _locationRepository.Find(originUnLocode);
Location destination = _locationRepository.Find(destinationUnLocode);
//...
container.Configure<Interception>()</pre>
//Ustaw sposób przechwytywania
.SetInterceptorFor<IBookingService>(new InterfaceInterceptor())
.SetInterceptorFor<IHandlingEventService>(new InterfaceInterceptor())
//Dodaj nową "politykę"
.AddPolicy("Transactions")
//Wykorzystującą aspekt obsługi transakcji
.AddCallHandler<TransactionCallHandler>()
//I podłącz do wszystkich interfejsów z assembly
.AddMatchingRule(new AssemblyMatchingRule("DDDSample.Application"));</div>
<div>
Który wykorzystując API sesji kontekstowych tworzy nową transakcję, wywołuje właściwą metodę, po czym, jeśli nie wystąpił żaden wyjątek, zatwierdza transakcję. W przypadku wyjątku transakcja pozostaje niezatwierdzona, a wyjątek (nienaruszony) przelatuje do warstw wyższych.
public class TransactionCallHandler : ICallHandler
{
private readonly ISessionFactory _sessionFactory;
public TransactionCallHandler(ISessionFactory sessionFactory)
{
_sessionFactory = sessionFactory;
}
public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
using (ITransaction tx = _sessionFactory.GetCurrentSession().BeginTransaction())
{
IMethodReturn result = getNext()(input, getNext);
if (result.Exception == null)
{
tx.Commit();
}
return result;
}
}
public int Order { get; set;}
}
Bardzo lubię ten kawałek kodu. Niestety nie sprawdza się on w moim ulubionym scenariuszu: NHibernate + NServiceBus. W takim przypadku potrzebuje transakcji rozproszonej System.Transactions. Swego czasu Ayende pisał o tym, że NHibernate “automagicznie” współdziała z System.Transactions. Ostatnio jednak pojawił się w NHibernate koncept ITransactionFactory, którego zadaniem jest (chyba?) poprawa jakości tego współdziałania. Domyślna implementacja fabryki, AdoNetWithDistrubtedTransactionFactory, jak sama nazwa sugeruje wspiera transakcje rozproszone. Niestety nie udało mi się sprawić, aby działała w najprostszym scenariuszu integracji z NServiceBus. Zrezygnowałem więc z tego ficzera i powróciłem do “starego dobrego” AdoNetTransactionFactory (który teraz trzeba sobie skonfigurować samemu). Niezbędna była jednak modyfikacja mojego TransactionCallHandler:
public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
IMethodReturn result;
SqlConnection sqlConnection = _sessionFactory.GetCurrentSession().Connection as SqlConnection;
if (sqlConnection == null)
{
throw new NotSupportedException("Only SqlConnection is supported.");
}
using (TransactionScope tx = new TransactionScope())
{
sqlConnection.EnlistTransaction(Transaction.Current);
result = getNext()(input, getNext);
if (result.Exception == null)
{
_sessionFactory.GetCurrentSession().Flush();
tx.Complete();
}
}
sqlConnection.EnlistTransaction(null);
return result;
}
Zamiast korzystać z API transakcji NHibernate, korzystam bezpośrednio z System.Transactions. Negatywnym skutkiem tego podejścia jest ograniczenie wspieranych baz do SQLServera 2005/2008. Niestety ADO.NET nie umożliwia niezależnego od sterownika bazy danych wpinania połączeń w transakcje rozproszone.
Pierwsze wywołanie EnlistTransaction wpina aktualne połączenie, na którym działa sesja NHibernate do transakcji rozproszonej. Drugie wywołanie (to z null-em) odłącza połączenie od transakcji. Jeśli transakcja ta nie została wcześniej zatwierdzona (tx.Complete()), zmiany są wycofywane na poziomie bazy danych.
Jest jeszcze jeden szkopuł. NHibernate domyślnie zwalnia połączenia związane z sesją najwcześniej, jak może. Jest to zachowanie optymalne z punktu widzenia wydajności, jednak w przypadku takiego zarządzania transakcjami, niepoprawne. Nie chcemy przecież, aby nasze połączenie, które podłączyliśmy do rozproszonej transakcji, zostało zamknięte. Aby temu zapobiec, musimy do konfiguracji NHibernate dodać następujący wpis:
<property name=“connection.release_mode“>on_close</property>
Na koniec jeszcze jedna niemiła informacja: powyższy sposób zarządzania transakcjami jest niekompatybilny z cache 2-go poziomu NHibernate. Co to oznacza? Otóż modyfikacje dokonane na danych w ramach tak zrealizowanych transakcji nie zostaną uwzględnione w cache 2-go poziomu. Nie sprawia to jednak problemu, jeśli nie modyfikujemy danych cache-owanych. Można więc próbować obejścia, polegającego na stosowaniu obu pokazanych wersji TransactionCallHandler w zależności od tego, czy transakcje rozproszone są wymagane.
Taka strategia powinna sprawdzać się dobrze, ponieważ wymaganie transakcji rozproszonych jest związane z odbieraniem / wysyłaniem komunikatów NServiceBus, a tego rodzaju akcje nie powinny modyfikować danych słownikowych (które są zwykle cache-owane).




about 1 year ago
Szymon. Czy można robić transkacyjne kolejki (konfiguracja NSB IsTransactional = true) ale bez użycia Ms Distributed Transaction Coordinator-a, w obrębie jednej maszyny ?
W moim projekcie mam właśnie NSB w konfiguracji IsTransactional = true no i gdy rozpoczynam transkację NHibernate poprzez TransactionScope() to zaczyna się włączać MSDTC, ale z jakimś błędem i klops.
Mogę ustawić NSB IsTransactional = false, wtedy wszysko jest ok, ale jak rozumiem ryzykuję utratę Message, gdy jej obsługa w handlerze nie powiedzie się, tak ?
about 1 year ago
A tak generalnie, to strasznie fajnie jak mi w google-u coś wartościowego po polsku zaczęło wyskakiwać na techniczne .Net-owe zapytania.
Mówię o twoim blogu, bo właśnie rozgryzam NServiceBus-a, uruchomiłem już i działa, mam już pierwsze ‘workflow-y’ obsługi płatności zrobione ale pewnie jeszcze dużo przede mną.
about 1 year ago
Niestety nie da się zachować transakcyjności operacji na kolejkach i bazie bez DTC, nawet lokalnie. Jeśli twoje środowisko zakłada, że serwer bazodanowy i kolejki znajdują się na jednej maszynie, to nie powinno być problemu z przekonaniem adminów do włączenia MS DTC wyłącznie dla dostępu lokalnego.
Z drugiej strony możesz próbować obejść wymaganie transakcyjności na poziomie biznesu: Twoje komunikaty mogą być idempotentny, dzięki czemu nie musisz odczytu z kolejki i zapisu do bazy robić w jednej transakcji — wystarczy, że transakcja kolejkowa zakończy się PO bazodanowej (gwarantuje to NSB). Wtedy w najgorszym wypadku ten sam komunikat zostanie dostarczony do Twojej usługi wielokrotnie. Jeśli jesteś sobie z tym w stanie poradzić, możesz spokojnie żyć bez transakcji rozproszonych.