(0) exportieren Drucken
Alle erweitern
Erweitern Minimieren

Transaktionen im .NET Framework 2.0

Veröffentlicht: 16. Jan 2006
Von Juval Lowy

Transaktionsprogrammierung wird gewöhnlich nur für datenbankzentrische Anwendungen genutzt. Für andere Typen von Anwendungen wurde bislang noch kein Vorteil aus diesem überlegenen Programmiermodell gezogen. Das.NET Framework 2.0 stellt eine rudimentäre Unterstützung von Managern für flüchtige Ressourcen bereit, mit denen Sie selbst Transaktionsunterstützung für arbeitsspeicherbasierte Ressourcen wie Klassenmember-Variablen verwenden können. Zu Beginn dieses Artikels erläutere ich kurz den Problembereich und die Motivation zur Verwendung von Transaktionen. Darüber hinaus werde ich einige Grundbegriffe wie "ACID" definieren. Ich werde das Konzept eines Ressourcen-Managers vorstellen und erklären, wie und in welchem Ausmaß das .NET Framework 2.0 Manager für flüchtige Ressourcen unterstützt.

Anschließend stelle ich die Grundbausteine zur richtigen Nutzung flüchtiger Ressourcen vor und erkläre, wie diese Grundbausteine zum Wrappen vorhandener Typen wie z. B. Auflistungen von System.Collections.Generic verwendet werden. In diesem Artikel wird nicht nur verstärkt Gebrauch von Visual C# ®2.0 gemacht, sondern auch schrittweise in einige fortgeschrittene Programmiertechniken für .NET Framework 2.0 eingeführt, einschließlich Typeneinschränkung, Klonen von Typen, Transaktionsereignisse und Ressourceneintragung.

Der Code kann hier heruntergeladen werden: Transactions.exe (148KB)

Auf dieser Seite

Problembereich Transaktionen Problembereich Transaktionen
Transaktionseigenschaften: ACID Transaktionseigenschaften: ACID
Transaktionen im .NET Framework 2.0 Transaktionen im .NET Framework 2.0
Manager für flüchtige Ressourcen Manager für flüchtige Ressourcen
Transaktionsbasierte Sperren Transaktionsbasierte Sperren
Serialisierung und Klonen Serialisierung und Klonen
Exemplarische Vorgehensweise in TransactionalT Exemplarische Vorgehensweise in Transactional<T>
Transaktionsauflistungen Transaktionsauflistungen
Schlussbemerkung Schlussbemerkung
Der Autor Der Autor

Problembereich Transaktionen

Richtige Fehlerbehandlung und Fehlerbehebung gehören zu den verwundbarsten Punkten vieler Anwendungen. Nachdem in einer Anwendung bei einem bestimmten Vorgang ein Fehler aufgetreten ist, sollte diese Anwendung das System in einem konsistenten Status wiederherstellen. Gewöhnlich ist dies der Status, in dem sich das System vor Ausführung des fehlgeschlagenen Vorgangs befunden hat. Typischerweise besteht jeder Vorgang, bei dem ein Fehler auftreten kann, aus mehreren kleinen Teilschritten. Einige dieser Schritte können fehlschlagen, nachdem andere erfolgreich ausgeführt wurden.

Das Problem bei der Wiederherstellung ist einfach die große Anzahl an Permutationen von zum Teil erfolgreichen und zum Teil fehlgeschlagenen Ausführungen, die Sie beim Schreiben des Codes berücksichtigen müssen. Der Versuch, Wiederherstellungscode in einer Anwendung von angemessener Größe manuell zu implementieren, ist meist aussichtslos und führt zu unsicherem Code. Dieser ist dann oft anfällig für jegliche Änderungen in der Anwendungs-Ausführung oder bei Verwendung im Geschäftsbereich und führt zu Beeinträchtigungen sowohl in der Produktivität als auch der Leistung. Da es außerdem darum geht, das System in einem konsistenten Status wiederherzustellen (gewöhnlich dem Status vor Ausführung des Vorgangs, bei dem der Fehler auftritt), müssen Sie sämtliche Schritte des Vorgangs rückgängig machen, die erfolgreich ausgeführt wurden. Wie werden jedoch Schritte wie das Löschen einer Zeile in einer Tabelle, eines Knotens in einer verknüpften Liste oder eines Elements für eine Auflistung rückgängig gemacht? Was ist außerdem, wenn vor dem Fehlschlag des Vorgangs auf Ihre Anwendungen zugegriffen und auf Grundlage des Systemstatus verfahren wird, für den Sie während der Wiederherstellung einen Rollback durchführen? Dieser Vorgang wird auf Grundlage von inkonsistenten Informationen durchgeführt und ist daher zwangsläufig fehlerhaft. Darüber hinaus stellt der fehlgeschlagene Vorgang unter Umständen nur einen Schritt in einem anderen, umfassenderen Vorgang dar, der sich über mehrere Komponenten von verschiedenen Anbietern über einer Reihe von Computern erstreckt. Wie würden Sie das gesamte System in einem solchen Fall wiederherstellen?

Gewährleistung der Konsistenz bei Verwendung von Transaktionen
Abbildung 1: Gewährleistung der Konsistenz bei Verwendung von Transaktionen

einzige) Option zur Gewährleistung der Systemkonsistenz und für den richtigen Umgang mit den Herausforderungen, die eine Fehlerbehebung mit sich bringen. Bei einer Transaktion handelt es sich um einen Satz potenziell komplexer Vorgänge, bei denen ein Fehler in einem einzelnen Vorgang zum Fehlschlag des gesamten Satzes als einem atomaren Vorgang führt. Wie in Abbildung 1 dargestellt, kann sich das System während der Ausführung der Transaktion zeitweise in einem inkonsistenten Zustand befinden. Nach Abschluss der Transaktion befindet sich das System jedoch sicher in einem konsistenten Zustand: entweder in einem neuen konsistenten Zustand (im Diagramm Zustand B) oder im ursprünglichen konsistenten Zustand, in dem sich das System vor dem Beginn der Transaktion befand (im Diagramm Zustand A). Wenn die Transaktion erfolgreich ist und das System vom konsistenten Status A in den konsistenten Status B übergeht, spricht man von einer übermittelten Transaktion. Wenn während der Ausführung der Transaktion ein Fehler auftritt und für alle bereits erfolgreichen Schritte ein Rollback durchgeführt wird, spricht man von einer abgebrochenen Transaktion. Wenn die Transaktion weder übermittelt noch abgebrochen wird, so heißt eine solche Transaktion ungewiss. In diesem Fall ist eine Unterstützung durch einen Administrator oder Benutzer erforderlich.

Transaktionsprogrammierung erfordert die Nutzung von Ressourcen wie z. B. einer Datenbank oder einer Nachrichtenwarteschlange, die in der Lage sind, für während der Transaktion vorgenommene Änderungen einen Commit oder Rollback durchzuführen. Solche Ressourcen gibt es in der einen oder anderen Form bereits seit Jahrzehnten. Gewöhnlich werden Ressourcen die Transaktionen angezeigt, für die diese eingesetzt werden sollen. Hierbei spricht man von einer Eintragung der Ressource in die Transaktion. Wenn Sie dann Aufgaben für diese Ressource durchführen und kein Fehler auftritt, fordern Sie bei der Ressource für die Änderungen an ihrem Status einen Commit an. Wenn ein Fehler auftritt, fordern Sie für die Änderungen einen Rollback an. Während einer Transaktion ist es von entscheidender Bedeutung, dass Sie auf keine Nicht-Transaktionsressourcen zugreifen (z. B. ein Nicht-Transaktionsdateisystem), da für Änderungen an solchen Ressourcen bei Abbruch der Transaktion kein Rollback durchgeführt werden kann.

Da der Systemstatus gewöhnlich im Arbeitsspeicher enthalten ist, müssen Objekte diesen Status proaktiv verwalten, wenn auf diesen während einer Transaktion zugegriffen wird. Eine Möglichkeit, dies zu bewerkstelligen, besteht darin, den gesamten Status in einer Transaktionsressource wie einer Datenbank zu speichern und für Statusänderungen als Teil der Transaktion einen Commit oder einen Rollback durchzuführen. Infolgedessen werden Transaktionen heutzutage nur für Anwendungen verwendet, die Ressourcen wie Transaktionsdatenbanken nutzen, obwohl Transaktionen ein überlegenes, stabiles und produktivitätsorientiertes Programmiermodell darstellen. Im Allgemeinen versprechen andere Arten von Anwendungen Wiederherstellungsfunktionen durch manuelle Erstellung von Lösungen für einen begrenzten Satz von Fehlern, mit denen Entwickler umgehen können, was zu Produktivitäts- und Qualitätseinbußen führt.

Transaktionseigenschaften: ACID

Die Transaktionen einer Ressource müssen über vier wichtige Eigenschaften verfügen: atomar, konsistent, isoliert und dauerhaft. Diese werden zusammengefasst als ACID-Eigenschaften bezeichnet (Atomic, Consistent, Isolated, Durable).

Mit "atomar" ist gemeint, dass bei Abschluss einer Transaktion alle Änderungen am Ressourcenstatus vorgenommen werden müssen, als ob diese zusammen einen unteilbaren Vorgang darstellen. Die Änderungen an der Ressource finden statt, während sämtliche Vorgänge angehalten werden. Nach Vornehmen der Änderungen wird mit allen Vorgängen fortgefahren. Nach Abschluss einer Transaktion sollten keine Tasks im Hintergrund verbleiben, da sonst die Atomarität verletzt wird. Jeder Vorgang, der aufgrund der Transaktion ausgeführt wird, muss in der Transaktion selbst enthalten sein.

Da Transaktionen atomar sind, wird die Entwicklung von Clientanwendungen erheblich vereinfacht. Der Client muss keine Fehler bei Teilschritten oder entsprechende Anforderungen verwalten oder über eine komplexe Wiederherstellungslogik verfügen. Der Client ermittelt, ob die Transaktion als Ganzes erfolgreich ausgeführt wird oder fehlschlägt. Im Falle eines Fehlschlags kann der Client auswählen, ob eine neue Anforderung (für den Start einer neuen Transaktion) durchgeführt oder beispielsweise der Benutzer gewarnt werden soll. Das Entscheidende hierbei ist, dass das System nicht vom Client wiederhergestellt werden muss.

"Konsistent" bedeutet, dass das System durch die Transaktion in einem logischen Zustand belassen wird. Beachten Sie, dass sich Konsistenz von Atomarität unterscheidet. Selbst wenn für alle Änderungen in Form eines atomaren Vorgangs ein Commit durchgeführt wird, muss die Transaktion gewährleisten, dass alle diese Änderungen im Systemkontext sinnvoll sind. Gewöhnlich ist es Sache des Entwicklers, sicherzustellen, dass die Semantik der Vorgänge konsistent sind. Die Transaktion muss lediglich das System von einem konsistenten Zustand in einen anderen übertragen.

Mit "isoliert" ist gemeint, dass keine andere Entität (ob transaktional oder nicht) in der Lage ist, den Zwischenstatus der Ressource während der Transaktion zu erfassen, da dieser möglicherweise inkonsistent ist. (Tatsächlich könnte auch bei Inkonsistenz des Ressourcenstatus die Transaktion immer noch abgebrochen und für die Änderungen ein Rollback durchgeführt werden.) Isolierung ist von entscheidender Bedeutung für die gesamte Systemkonsistenz. Angenommen, Transaktion A ermöglicht Transaktion B den Zugriff auf ihren Zwischenstatus. Transaktion A wird anschließend abgebrochen und Transaktion B führt einen Commit durch. Das Problem hierbei ist, dass Transaktion B auf Grundlage eines Systemstatus ausgeführt wird, für den ein Rollback durchgeführt wurde. Dies führt zu einer unbeabsichtigten Inkonsistenz von Transaktion B.

Isolierungsverwaltung ist keine triviale Angelegenheit. Die an einer Transaktion beteiligten Ressourcen müssen Daten, auf die von der Transaktion zugegriffen werden, für sämtliche andere Zugriffe sperren. Des Weiteren muss der Zugriff auf diese Daten wieder entsperrt werden, wenn die Transaktion übermittelt oder abgebrochen wird. Theoretisch sind mehrere Stufen der Transaktionsisolierung möglich. Im Allgemeinen verstärkt das Maß an Isolierung die Konsistenz der Ergebnisse einer Transaktion.

Transaktionen verwenden in .NET Framework 2.0 standardmäßig die höchste Isolierungsstufe, die "serialisiert" genannt wird. Das heißt, dass die Ergebnisse eines Satzes von parallelen Transaktionen mit den Ergebnissen übereinstimmen, die man durch serielle Ausführung der Transaktionen erhält. Um Serialisierung zu erreichen, werden alle von einer Transaktion verwendeten Ressourcen für jede andere Transaktion gesperrt. Wenn andere Transaktionen versuchen, auf diese Ressourcen zuzugreifen, werden diese gesperrt und können nicht weiter ausgeführt werden, bis die ursprüngliche Transaktion übermittelt oder abgebrochen wird.

"Dauerhaft" bedeutet, dass die Ergebnisse einer erfolgreichen Transaktion im System beibehalten werden. Es kann jederzeit passieren, dass die Anwendung abstürzt und der verwendete Arbeitsspeicher gelöscht wird. Wenn die Änderungen am Systemstatus im Arbeitsspeicher vorgenommen wurden, gehen diese dabei verloren, und das System befindet sich in einem inkonsistenten Zustand. Selbst wenn die Informationen im Dateisystem gespeichert werden und diese einen Anwendungsabsturz überstehen können, gehen die Änderungen bei einem Datenträgerfehler verloren. Und obwohl Sie Datenträgerfehlern mit redundanten Laufwerken begegnen können, ist diese Methode bei einem Brand im Serverraum kaum von Nutzen. Um für solche Fälle gewappnet zu sein, können Sie für die Ressourcen mehrere gespiegelte Sites verwenden und diese an Standorten platzieren, in denen keine Erdbeben oder Überschwemmungen vorkommen.

Wie Sie sehen können, stehen für Dauerhaftigkeit eine Reihe von Optionen zur Verfügung. Wie widerstandsfähig die Ressource gegen solche Katastrophen sein sollte, ist eine offene Frage, die abhängig ist von der Art und Vertraulichkeit der Daten, dem vorhandenen Budget, der verfügbaren Zeit, dem verfügbaren Personal zur Systemadministration usw. Wenn Dauerhaftigkeit tatsächlich mehrere verschiedene Stufen von Persistenz umfasst, sollten Sie eine der Extreme des Spektrums in Betracht ziehen: flüchtige Ressourcen im Arbeitsspeicher. Der Vorteil flüchtiger Ressourcen besteht darin, dass diese eine höhere Leistung als beständige Ressourcen bieten. Wichtiger noch ist, wie in diesem Artikel beschrieben, dass flüchtige Ressourcen bei Verwendung von Transaktionsunterstützung für die Fehlerbehebung eine bessere Annäherung an konventionelle Programmiermethoden ermöglichen.

Transaktionen im .NET Framework 2.0

Das .NET Framework 2.0 automatisiert die Eintragung und Verwaltung einer Transaktion für Transaktionsressourcen. Der System.Transactions-Namensraum stellt eine gemeinsame Infrastruktur für Transaktionsklassen, häufige Verhaltensweisen und Hilfsklassen zur Verfügung. Das .NET Framework definiert einen Ressourcen-Manager als Ressource, die automatisch in einer von System.Transactions verwalteten Transaktion eingetragen werden kann. Die Ressource wird in die Transaktion eingetragen, wenn auf sie während der Transaktion zugegriffen wird. Der System.Transactions-Namensraum unterstützt ein Konzept namens Umgebungstransaktion. Hierbei handelt es sich um die Transaktion, in der der Code ausgeführt wird (und die im threadlokalen Speicher gespeichert wird). Um einen Verweis auf die Umgebungstransaktion zu erhalten, rufen Sie die statische Current-Eigenschaft der Transaction-Klasse auf:

Transaction ambientTransaction = Transaction.Current;

Wenn keine Umgebungstransaktion vorhanden ist, gibt Current den Wert NULL zurück. In Abbildung 2 werden die wichtigsten Teile der Transaction-Klasse dargestellt. Um Ressourcen für Transaktionen einzusetzen, verwenden Sie die wie folgt definierte TransactionScope-Klasse:

public class TransactionScope : IDisposable
{
   public void Complete();
   public void Dispose();
   public TransactionScope();
   ... // Additional constructors
}

Wie der Name sagt, wird die TransactionScope-Klasse verwendet, um einen Codeabschnitt mit einer Transaktion in einem Gültigkeitsbereich (Scope) einzuschließen:

using(TransactionScope scope = new TransactionScope())
{
   ... // Perform transactional work here

   // No errors - commit transaction
   scope.Complete();
}

Im Konstruktor des TransactionScope-Objekts wird ein Transaktionsobjekt erstellt und dieses als Umgebungstransaktion zugeordnet, indem die statische Current-Eigenschaft der Transaction-Klasse festgelegt wird. TransactionScope ist ein entfernbares Objekt: Die Transaktion wird beendet, sobald die Dispose-Methode aufgerufen wird (das Ende des using-Blocks).

Das TransactionScope-Objekt ist nicht in der Lage zu ermitteln, ob die Transaktion übermittelt oder abgebrochen werden sollte. Deshalb verfügt jedes TransactionScope-Objekt über ein Konsistenzbit, das standardmäßig auf FALSE festgelegt wird. Das Konsistenzbit kann durch Aufrufen der Complete-Methode auf TRUE gesetzt werden. (Da dies die letzte Zeile im Gültigkeitsbereich ist, wird der Aufruf gewöhnlich von allen bis zu diesem Punkt ausgelösten Ausnahmen übersprungen.) Wenn die Transaktion beendet wird und das Konsistenzbit auf FALSE gesetzt ist, wird die Transaktion abgebrochen. Beispielsweise führt das folgende Bereichsobjekt für die entsprechende Transaktion einen Rollback durch, da der Standardwert des Konsistenzbits an keiner Stelle geändert wird:

using(TransactionScope scope = new TransactionScope())
{
}

Wenn Sie andererseits Complete aufrufen und die Transaktion mit einem auf TRUE gesetzten Konsistenzbit beenden, wird die Transaktion übermittelt.

System.Transactions ist einfach zu handhaben, da Sie kaum mit dem Transaktionsobjekt interagieren oder explizit Ressourcen eintragen müssen. Des Weiteren müssen Sie auch nicht darauf achten, ob Sie mit einem Teil einer lokalen oder verteilten Transaktion arbeiten. Als System.Transactions-Transaktions-Ressourcen-Manager verwendet dieser beim Zugriff auf die zugrunde liegende dauerhafte Ressource Transaction.Current, um einen Verweis auf die Umgebungstransaktion zu erhalten. Anschließend wird eine der EnlistDurable-Methoden aufgerufen, so dass eine Implementierung von IEnlistmentNotification übergeben wird, wie in Abbildung 3 dargestellt.

Der System.Transactions-Transaktions-Manager übergibt mit IEnlistmentNotification dem Ressourcen-Manager das Ergebnis der Transaktion und fordert deren Übermittlung oder Abbruch an. Darüber hinaus wird IEnlistmentNotification von System.Transactions verwendet, um das Zweiphasen-Commit-Protokoll zu verwalten. Wenn alle beteiligten Objekte in der Transaktion für deren Übermittlung stimmen, ruft der Transaktions-Manager für jede Ressource die Prepare-Methode auf:

void Prepare(PreparingEnlistment preparingEnlistment);

Durch Prepare wird vorher ermittelt, ob jede Ressource für die Änderungen gegebenenfalls einen Commit durchführen kann. Hierbei handelt es sich um die erste Phase des Zweiphasen-Commit-Protokolls. Wenn die Transaktion von der Ressource abgebrochen werden soll, wird die ForceRollback-Methode des bereitgestellten PreparingEnlistment-Objekts aufgerufen. Wenn die Ressource in der Lage ist, für die Änderungen einen Commit durchzuführen, wird die Prepared-Methode aufgerufen.

Wenn in der zweiten Phase des Protokolls alle Ressourcen-Manager für die Übergabe der Transaktion stimmen, ruft der Transaktions-Manager die Commit-Methode der IEnlistmentNotification-Schnittstelle auf. Wenn einer der Ressourcen-Manager für den Abbruch der Transaktion stimmt, ruft der Transaktions-Manager die Rollback-Methode auf. Dadurch ist System.Transactions in der Lage, sowohl Atomarität als auch Konsistenz für mehrere Ressourcen zu gewährleisten.

Da die Verwendung des Zweiphasen-Commit-Protokolls für nur eine einzelne Ressource nicht sinnvoll ist, stellt System.Transactions außerdem die ISinglePhaseNotification-Schnittstelle bereit:

public class SinglePhaseEnlistment : Enlistment
{
   public void Aborted();
   public void Committed();
   ... // Additional methods
}

public interface ISinglePhaseNotification : IEnlistmentNotification
{
   void SinglePhaseCommit(SinglePhaseEnlistment singlePhaseEnlistment);
}

Der Ressourcen-Manager kann sich für die Implementierung von ISinglePhaseNotification entscheiden und die Eintragung mithilfe einer der enlisting-Methoden von Transaction vornehmen, die ISinglePhaseNotification verwenden. Wenn es sich bei der Ressource um die einzige Ressource in der Transaktion handelt, ruft System.Transactions lediglich die SinglePhaseCommit-Methode auf und verwendet das Zweiphasen-Commit-Protokoll nicht. In diesem Fall muss die Ressource dem Transaktions-Manager mithilfe des SinglePhaseEnlistment-Parameters das Ergebnis des Commit-Versuchs mitteilen.

Manager für flüchtige Ressourcen

System.Transactions ermöglicht außerdem die Verwendung von Managern für flüchtige Ressourcen, deren Status im Arbeitsspeicher gespeichert wird. Obwohl deren Status nicht beständig ist, können Manager für flüchtige Ressourcen oftmals sehr viel mehr Anwendungen versorgen als beständige Ressourcen. Wenn es sich bei Klassenmember-Variablen (oder methodenlokalen Variablen) um flüchtige Ressourcen handelt, können Sie diese innerhalb der Transaktion aufrufen und auf diese Weise die Transaktions-Programmierung erheblich vereinfachen.

In Hinblick auf System.Transactions muss eine flüchtige Ressource lediglich IEnlistmentNotification implementieren, den Zugriff einer Transaktion auf IEnlistmentNotification erfassen und durch Aufrufen der VolatileEnlist-Methode des Transaktionsobjekts eine automatische Eintragung in die Transaktion durchführen.

Für die Implementierung von IEnlistmentNotification sind einige Hürden zu überwinden. Im Idealfall sollten alle Datentypen transaktional sein, d.h. transaktionale Ganzzahlen, transaktionale Zeichenfolgen, transaktionale Customer-Objekte usw.:

public class TransactionalInteger : IEnlistmentNotification
{...}
public class TransactionalString  : IEnlistmentNotification
{...}
public class TransactionalCustomer : IEnlistmentNotification
{...}

Dazu müsste jedoch jeder Typ, der in die Transaktion eingetragen werden soll, IEnlistmentNotification implementieren. Aus diesem Grund ist es wesentlich vorteilhafter, generische Typen zu verwenden und eine generische Implementierung von IEnlistmentNotification bereitzustellen. Um dem Rechnung zu tragen, habe ich eine Transactional<T>-Klasse geschrieben:

public class Transactional<T> : IEnlistmentNotification
{
   public Transactional(T value);
   public Transactional();
   public T Value { get; set; }
   public static implicit operator T(Transactional<T> transactional);
   ... // More members 
}

Transactional<T> ist ein vollständiger Manager für flüchtige Ressourcen, der automatisch in eine Umgebungstransaktion eingetragen wird und für beliebige Änderungen an seinem Status, die entsprechend des Transaktionsergebnisses vorgenommen werden, einen Commit oder ein Rollback durchführt.

Mit der Value-Eigenschaft von Transactional<T> können Sie auf den zugrunde liegenden Typ zugreifen und jede unterstützte Methode oder unterstützten Operator aufrufen, wie in Abbildung 4 dargestellt. Für die Werte der m_Number-Klassenmembervariablen und der lokalen city-Variablen wird ein Rollback auf deren Status vor der Transaktion durchgeführt.

Bei der Implementierung von Managern für flüchtige Ressourcen wie Transactional<T> treten ein paar Probleme auf, die gelöst werden müssen. Zunächst einmal ist da das Problem der Isolierung. Die flüchtige Ressource muss über die Isolierungseigenschaft von ACID verfügen, die verwaltete zugrundeliegende Ressource sperren und den Zugriff durch mehrere Transaktionen verhindern. .NET bietet jedoch nur threadbasierte Sperren, die nur den Zugriff durch parallele Threads und nicht durch parallele Transaktionen verhindern. Das zweite Problem ist die Statusverwaltung. Der Ressourcen-Manager muss die Transaktion berechtigen, den Status der Ressource zu ändern und dennoch in der Lage sein, für solche Änderungen einen Rollback durchzuführen, wenn die Transaktion abgebrochen wird.

Transaktionsbasierte Sperren

Da das .NET Framework 2.0 keine transaktionsbasierte Sperre bereitstellt, habe ich selbst eine geschrieben. Ich habe sie TransactionalLock genannt und sie mit einer einfachen Schnittstelle versehen:

public class TransactionalLock
{
   public void Lock();
   public void Unlock();
   public bool Locked { get; }
}

TransactionalLock ermöglicht exklusives Sperren und unterstützt somit Isolierung auf Serialisierungsebene. Als Eigentümer der Sperre ist nur jeweils eine Transaktion zulässig. Die Sperre wird durch Aufrufen der Lock-Methode abgerufen und durch Aufrufen der Unlock-Methode aufgehoben. Wenn eine andere Transaktion versucht, die Sperre abzurufen, ist diese blockiert. Wenn dieselbe Transaktion mehrere Male Lock aufruft, nachdem die Sperre abgerufen wurde, hat dies keine weiteren Auswirkungen. Wenn mehrere ausstehende Transaktionen vorhanden sind, die versuchen, die Sperre abzurufen, werden diese alle blockiert. Sie werden dann in einer Warteschlange platziert und der Reihe nach als Eigentümer der Sperre berechtigt. Wenn eine Transaktion Eigentümer der Sperre ist, blockiert TransactionalLock auch nicht-transaktionale Aufrufe und platziert diese Aufrufer ebenfalls in der Warteschlange. Wenn die Transaktion abgeschlossen wird, während diese auf den Abruf der Sperre wartet, wird die Blockierung der Transaktion aufgehoben.

Es sollte darauf hingewiesen werden, dass TransactionalLock transaktionsbasierten synchronisierten Zugriff und keinen threadbasierten Zugriff bereitstellt. Das .NET Framework 2.0 ermöglicht die Ausführung mehrerer Threads im Gültigkeitsbereich derselben Transaktion, so dass mehrere Threads Eigentümer der Sperre sein können, solange sich diese in derselben Transaktion befinden. Wenn darüber hinaus ein Thread TransactionalLock entsperrt, geschieht dies für alle anderen Threads in der Transaktion, gleichgültig, wie oft Lock aufgerufen wurde. Dies geschieht mit Absicht, um die automatische Entsperrung auf Grundlage des Transaktionsabschlusses zu erleichtern. Wenn eine Multithread-Anwendung direkt mit TransactionalLock interagiert, müssen sich die Threads untereinander koordinieren, wenn der Aufruf von Unlock zulässig ist.

In Abbildung 5 wird die Implementierung von TransactionalLock dargestellt. Der Kürze halber ist nicht der gesamte Code dargestellt. Bei Abbildung 6 und Abbildung 7 handelt es sich um UML-Aktivitätsdiagramme, in denen die Lock-Methode bzw. die Unlock-Methode dargestellt wird.

Funktionsweise der "Lock"-Methode
Abbildung 6: Funktionsweise der "Lock"-Methode

Werfen Sie zunächst einmal einen Blick auf die Lock-Methode. TransactionalLock verfügt über die m_OwningTransaction-Membervariable vom Transaction-Typ, und die OwningTransaction-Eigenschaft stellt threadsicheren Zugriff darauf bereit. Solange OwningTransaction den Wert NULL hat, gilt die Sperre als aufgehoben. Wenn die Lock-Methode aufgerufen wird und OwningTransaction den Wert NULL hat, wird OwningTransaction von TransactionalLock einfach auf die aktuelle Transaktion festgelegt.

Methode "Unlock"
Abbildung 7: Methode "Unlock"

Wenn OwningTransaction nicht NULL ist, überprüft TransactionalLock, ob die eingehende (durch Aufrufen von Transaction.Current erhaltene) Transaktion mit der besitzenden Transaktion übereinstimmt. In diesem Fall hat die Sperre keine Auswirkungen. Wenn es sich andererseits bei der eingehenden Transaktion um eine andere als die besitzende Transaktion handelt, fügt TransactionalLock diese Transaktion einer privaten Warteschlange hinzu, die von TransactionalLock verwaltet wird: m_PendingTransactions. Hierbei handelt es sich einfach um eine verknüpfte Liste, in der Schlüssel-/Wertpaare gespeichert werden. Der Schlüssel für jedes Paar ist die Transaktion, die versucht hat, die bereits einem Eigentümer zugeordnete Sperre abzurufen, und der Wert ist ein manuelles Zurücksetzungs-Ereignis. Die Idee dahinter ist, dass die aufrufende Transaktion blockiert wird, indem diese auf das Ereignis wartet, das von der Unlock-Methode signalisiert wird. Das Problem dabei ist, dass die blockierte Transaktion abgebrochen werden kann (z. B. von einem anderen Thread in der Transaktion), oder sie wird einfach aufgrund einer Zeitüberschreitung beendet. Wie kann der aufrufende Transaktions-Thread also feststellen, dass er abgebrochen wurde, wenn dieser blockiert ist?

Glücklicherweise stellt die Transaction-Klasse das TransactionCompleted-Ereignis bereit, welches Sie abonnieren können (siehe Abbildung 2). Dieses Ereignis wird für einen vom Transaktions-Manager verwalteten Thread ausgelöst. Das Ereignis wird von der Lock-Methode mithilfe einer anonymen Methode abonniert. Wenn sich die Transaktion immer noch in der Warteschlange befindet, wird die Transaktion von der anonymen Methode aus der Warteschlange entfernt, und ein manuelles Zurücksetzungsereignis wird signalisiert. Die Funktionsweise der Unlock-Methode ist sogar noch einfacher: Zunächst wird die Sperre zurückgesetzt, indem OwningTransaction auf NULL gesetzt wird. Unlock entfernt anschließend vom Anfang der m_PendingTransactions-Warteschlange das nächste Transaktions-/Übereinstimmungspaar. Unlock ruft die Sperre für die Transaktion ab, die in der Warteschlange als nächstes an der Reihe war und hebt die Blockierung dieser Transaktion durch Signalisieren des manuellen Zurücksetzungsereignisses auf.

Serialisierung und Klonen

Das zweite Problem beim Implementieren von Transactional<T> ist die Unterstützung von Rollbacks für den Status der zugrundeliegenden Ressource, wenn eine Transaktion abgebrochen wird. Darüber hinaus muss Transactional<T> in der Lage sein, für die Änderungen einen Commit in Form eines atomaren Vorgangs durchzuführen. Ich habe mich dazu entschlossen, eine temporäre Kopie des Ressourcenstatus zu erstellen, mit der die Transaktion arbeiten kann. Wenn die Transaktion abgebrochen wird, löscht Transactional<T> die Kopie ganz einfach. Wenn die Transaktion übermittelt wird, verwendet Transactional<T> die temporäre Kopie als neuen Status der Ressource.

Die Frage lautet nun: Wie erstellen Sie eine Kopie des Ressourcen-Status? Beim Umgang mit Verweistypen reicht der Zuweisungsoperator nicht aus, da dieser nur den Verweis kopiert. Eine Einschränkung des Typs für die Unterstützung von ICloneable ist ebenfalls unzureichend, da keine Information darüber vorliegt, ob der zurückgegebene Klon alle Informationen enthält oder eine oberflächliche Kopie des Verweises ist. (Typensicherheit ist ebenfalls nicht gewährleistet, da IClonable.Clone Werte vom Typ Object zurückgibt.) Die beste Lösung ist Serialisierung, da durch Serialisierung eine vollständige Kopie des Objekts und allen zugehörigen Membern erstellt wird. Sie können ein Objekt in einen Stream serialisieren und eine vollständige Kopie des Objekts aus dem Stream deserialisieren. Diese Sequenz wird von der statischen, generischen Clone-Methode der statischen Hilfsklasse ResourceManager gekapselt, wie in Abbildung 8 dargestellt. Der Kürze halber ist nicht der gesamte Code dargestellt. Clone verwendet einen Parameter namens source vom Typ T, serialisiert und deserialisiert diesen im Speicherstream mithilfe des binären Formatierungsprogramms und gibt das geklonte Objekt zurück.

Die Verwendung der Serialisierung als Klonmechanismus hat eine wichtige Konsequenz: Transactional<T> funktioniert nur mit serialisierbaren Typen. Leider wird die Einschränkung eines Typparameters auf einen serialisierbaren Typ von C# 2.0 nicht unterstützt. Um dies auszugleichen, überprüft Transactional<T> den Typparameter T im entsprechenden statischen Konstruktor mithilfe der ConstrainType-Methode von ResourceManager (siehe Abbildung 9). Bevor irgendetwas anderes mit Transactional<T> geschieht, wird der statische Konstruktor aufgerufen, und es ist eine gängige Methode, Einschränkungen zu erzwingen, die zur Kompilierungszeit nicht unterstützt werden. ConstrainType stellt sicher, dass der vorgegebene Typ serialisierbar ist, indem auf die IsSerializable-Eigenschaft des Typs zugegriffen wird.

Exemplarische Vorgehensweise in Transactional<T>

Sobald die wichtigsten Elemente der Transaktionssperrung und des Klonens des Ressourcenstatus zur Verfügung stehen, kann Transactional<T> implementiert werden. In Abbildung 9 wird die Implementierung dargestellt. Der Kürze halber ist nicht der gesamte Code dargestellt.

Transactional<T> verfügt über zwei Membervariablen vom Typ T. m_Value stellt den tatsächlichen konsistenten Zustand dar, und m_TemporaryValue ist die temporäre Kopie, die einer Transaktion übergeben wird. Transactional<T> verfügt außerdem über den Member m_Lock vom Typ TransactionalLock, durch den m_Value isoliert und nur serialisierter Zugriff auf m_Value ermöglicht wird. Schließlich verwaltet Transactional<T> einen Verweis auf die aktuelle Transaktion in der Membervariablen m_CurrentTransaction. Wenn m_CurrentTransaction nicht Null ist, gilt Transactional<T> als in einer Transaktion eingetragen.

Die Value-Eigenschaft delegiert an die Methoden GetValue und SetValue. Abbildung 10 stellt die Funktionsweise der Methode SetValue dar. Zunächst sperrt SetValue die Transaktionssperre m_Lock. Wenn eine andere Transaktion Eigentümer der flüchtigen Ressource ist, wird SetValue blockiert, bis SetValue auf die Ressource zugreift. Sobald die Sperre abgerufen wird, überprüft SetValue, ob diese bereits in einer Transaktion eingetragen ist. Wenn m_CurrentTransaction Null ist, und der Aufruf über keine Transaktion verfügt, legt SetValue den tatsächlichen m_Value-Wert fest, da keine Transaktionsunterstützung erforderlich ist. Wenn andererseits Transactional<T> nicht eingetragen ist, der Aufruf jedoch über eine Transaktion verfügt, wird SetValue in der Transaktion durch Aufrufen der Enlist-Hilfsmethode eingetragen. GetValue funktioniert ähnlich, außer dass GetValue aus der Ressource liest anstatt in diese zu schreiben.

Funktionsweise von "SetValue"
Abbildung 10: Funktionsweise von "SetValue"

Die Enlist-Methode legt m_CurrentTransaction auf die eingehende aktuelle Transaktion fest und ruft die EnlistVolatile-Methode der aktuellen Transaktion auf. Auf diese Weise wird die Enlist-Methode mit dem Transaktions-Manager von System.Transactions verknüpft. Schließlich erstellt Enlist mit ResourceManager.Clone eine vollständige Kopie der Ressource und ordnet diese m_TemporaryValue zu. Beachten Sie, dass EnlistVolatile eine Implementierung von IEnlistmentNotification akzeptiert, die von Transactional<T> explizit implementiert wird. IEnlistmentNotification wird von der Transaktion verwendet, um bei Transactional<T> eine Abstimmung während des Zweiphasen-Commit-Protokolls anzufordern und Transactional<T> über das Ergebnis der Transaktionen zu informieren. (Transactional<T> wird auf diese Weise angewiesen, die Transaktionen zu übermitteln oder abzubrechen.) Da Transactional<T> immer einen Commit durchführt, wenn dieser angefordert wird, ruft Prepare einfach die Prepared-Methode des vorbereitenden Elements auf.

Die Implementierungen von IEnlistmentNotification.Commit und IEnlistmentNotification.Rollback sind nun problemlos zu bewerkstelligen. Commit entfernt m_Value (wenn m_Value IDisposable bereitstellt), kopiert den Verweis auf den temporären Wert in m_TemporaryValue zum tatsächlichen m_Value-Wert und übermittelt auf diese Weise die Transaktion. Anschließend wird die Sperre aufgehoben, so dass eine weitere Transaktion den Wert der flüchtigen Ressource festlegen oder abrufen kann. IEnlistmentNotification.Rollback entfernt einfach m_TemporaryValue, legt dann m_Value auf seinen Standardwert fest und verwirft auf diese Weise die Änderungen. Anschließend wird die Sperre aufgehoben.

Transaktionsauflistungen

Beim generischen Typparameter T für Transactional<T> kann es sich um einen beliebigen serialisierbaren Typ handeln. Sie werden ihn wahrscheinlich jedoch nur mit zwei Hauptkategorien von Datenstrukturen oder Ressourcen verwenden. Zur ersten Kategorie zählen primitive Typen wie Ganzzahlen und Zeichenfolgen oder benutzerdefinierte Objekte wie Customer oder Order. Da eine unbegrenzte Anzahl solcher Typen vorhanden ist, reicht das Angebot von Transactional<T> aus. Wenn ein Typ serialisierbar ist, können Sie ihn als Manager für flüchtige Ressourcen verwenden und mit Value auf ihn zugreifen.

Die zweite Kategorie ist eine Auflistung einzelner Elemente wie Arrays, verknüpfte Listen, Warteschlangen usw. Sie können eine solche Auflistung als Typparameter angeben und auf diesen über die Value-Eigenschaft zugreifen, wie im Folgenden dargestellt:

Transactional<int[]> numbers = new Transactional<int[]>(new int[3]);
numbers.Value[0] = 1;
numbers.Value[1] = 2;
numbers.Value[2] = 3;

using(TransactionScope scope = new TransactionScope())
{
   numbers.Value[0] = 11;
   numbers.Value[1] = 22;
   numbers.Value[2] = 33;
   scope.Complete();
}
Debug.Assert(numbers.Value[2] == 33);

Wenn Sie den Typparameter auf diese Weise verwenden, führt dies jedoch zu einer etwas umständlichen Programmierung. Ein Transaktions-Array sollte wie ein normales Array, eine transaktionale verknüpfte Liste sollte wie eine normale verknüpfte Liste verwendet werden können, ohne dass stets eine Dereferenzierung mit Value erforderlich ist. Da es nur wenige dieser nützlichen Auflistungen gibt, werden alle Auflistungen in System.Collections.Generic als Transaktionsauflistungen definiert. Abbildung 11 zeigt TransactionalQueue als Beispiel.

Diese Auflistungen entsprechen vollständig den in System.Collections.Generic verfügbaren Auflistungen. Sie bieten dieselben Methoden und implementieren implizit oder explizit dieselben Schnittstellen wie ihre entsprechenden nicht-transaktionalen Verwandten. Aus diesem Grund werden Sie auf die gleiche Weise verwendet. Hier ist ein Beispiel:

TransactionalArray<int> numbers = new TransactionalArray<int>(3);
numbers[0] = 1;
numbers[1] = 2;
numbers[2] = 3;

using(TransactionScope scope = new TransactionScope())
{
   numbers[0] = 11;
   numbers[1] = 22;
   numbers[2] = 33;
}
Debug.Assert(numbers[2] == 3);

Abbildung 12 stellt ein Beispiel für die Verwendung von TransactionalQueue als Nachrichtenwarteschlange dar. Jede der Warteschlange hinzugefügte Nachricht wird abgewiesen, wenn die Transaktion, die die Einreihung in die Warteschlange vornimmt, abgebrochen wird. Jede aus der Warteschlange entfernte Nachricht wird dieser wieder hinzugefügt, wenn die Transaktion, die die Entfernung aus der Warteschlange vornimmt, abgebrochen wird. Dadurch werden Programmiermodelle möglich, die System.Messaging und Windows Communication Foundation MSMQ-Bindung entsprechen und über die folgenden Funktionen verfügen: Automatisches Abbrechen (für die Einreihung in der Warteschleife wird ein Rollback durchgeführt), garantierte Übermittlung sowie einen Mechanismus für automatische Wiederholungsversuche (wenn die Verarbeitung der Nachricht fehlschlägt, wird für die Entfernung aus der Warteschlange ein Rollback durchgeführt).

Um Teile der Implementierung zu automatisieren (alle Auflistungen müssen IEnumerable<T> unterstützen), werden die Transaktionsauflistungen von der abstrakten Klasse TransactionalCol-lection<C,T> abgeleitet, die wiederum von Transactional<C> abgeleitet wird, welche C als Typparameter übergibt. C unterstützt lediglich IEnumerable<T>, und Value wird im Konstruktor ein Wert vom Typ C zugewiesen. Aus diesem Grund kann TransactionalCollection<C,T> die Implementierung von IEnumerable<T> an Value delegieren. In jeder dieser konkreten Transaktionsauflistungen wird die Implementierung der verschiedenen Methoden und Eigenschaften durch Delegieren der Implementierung an Methoden und Eigenschaften bewerkstelligt, die nun von Value offen gelegt werden. Für jede konkrete Transaktionsauflistung legt Value die erforderlichen Methoden offen, da mit jeder konkreten Transaktionsauflistung die Auflistung angegeben wird, die als C-Typparameter beim Ableiten von TransactionalCollection<C,T> gewrappt wird:

public class TransactionalList<T> : TransactionalCollection<List<T>,T>,
                                    IList<T>,ICollection<T>
{...}

Sie können TransactionalCollection<C,T> als Basisklasse für eigene, benutzerdefinierte Transaktionsauflistungen verwenden.

Schlussbemerkung

Ich denke, dass Transaktionsprogrammierung von beständigen zu flüchtigen Ressourcen sich in Zukunft als Programmiermodell durchsetzen wird. Mit Managern für flüchtige Ressourcen können Sie die bedeutenden Vorteile von Transaktionen für alltägliche Objekte und gewöhnliche Datenstrukturen nutzen - von Ganzzahlen bis zu Dictionarys. Um Transaktionsfunktionen hinzuzufügen, sind nur wenige Änderungen am konventionellen Programmiermodell erforderlich. Dadurch, dass die Fehlerbehandlung und -behebung nicht mehr manuell erfolgen muss, erzielen Sie mehr Produktivität und Qualität sowie kürzere Entwicklungszeiten.

Der Autor

Juval Lowy ist Softwarearchitekt, der Beratung im Bereich .NET-Architektur und fortgeschrittene Schulungen anbietet. Er ist Microsoft Regional Director für Silicon Valley. Sein neuestes Buch heißt Programming .NET Components, 2nd Edition (O'Reilly, 2005) (in englischer Sprache). Sie erreichen Juval unter http://www.idesign.net/ (in englischer Sprache).


Anzeigen:
© 2014 Microsoft