Implementieren einer impliziten Transaktion mit Transaktionsbereich
Mit der TransactionScope-Klasse lassen sich Codeblöcke einfach als an einer Transaktion beteiligte Codeblöcke markieren, ohne die Transaktion selbst bearbeiten zu müssen. Ein Transaktionsbereich kann die Ambient-Transaktion automatisch auswählen und verwalten. Wegen ihrer einfachen Verwendung und Effizienz wird empfohlen, die TransactionScope-Klasse zur Entwicklung von Transaktionsanwendungen zu verwenden.
Außerdem müssen Sie keine Ressourcen für die Transaktion explizit eintragen. Jeder System.Transactions-Ressourcen-Manager (z. B. SQL Server 2005) kann das Vorhandensein einer vom Bereich erstellten Ambient-Transaktion erkennen und diese automatisch eintragen.
Erstellen eines Transaktionsbereichs
Es folgt ein Beispiel für die einfache Verwendung der TransactionScope-Klasse.
// This function takes arguments for 2 connection strings and commands to create a transaction
// involving two SQL Servers. It returns a value > 0 if the transaction is committed, 0 if the
// transaction is rolled back. To test this code, you can connect to two different databases
// on the same server by altering the connection string, or to another 3rd party RDBMS by
// altering the code in the connection2 code block.
static public int CreateTransactionScope(
string connectString1, string connectString2,
string commandText1, string commandText2)
{
// Initialize the return value to zero and create a StringWriter to display results.
int returnValue = 0;
System.IO.StringWriter writer = new System.IO.StringWriter();
try
{
// Create the TransactionScope to execute the commands, guaranteeing
// that both commands can commit or roll back as a single unit of work.
using (TransactionScope scope = new TransactionScope())
{
using (SqlConnection connection1 = new SqlConnection(connectString1))
{
// Opening the connection automatically enlists it in the
// TransactionScope as a lightweight transaction.
connection1.Open();
// Create the SqlCommand object and execute the first command.
SqlCommand command1 = new SqlCommand(commandText1, connection1);
returnValue = command1.ExecuteNonQuery();
writer.WriteLine("Rows to be affected by command1: {0}", returnValue);
// If you get here, this means that command1 succeeded. By nesting
// the using block for connection2 inside that of connection1, you
// conserve server and network resources as connection2 is opened
// only when there is a chance that the transaction can commit.
using (SqlConnection connection2 = new SqlConnection(connectString2))
{
// The transaction is escalated to a full distributed
// transaction when connection2 is opened.
connection2.Open();
// Execute the second command in the second database.
returnValue = 0;
SqlCommand command2 = new SqlCommand(commandText2, connection2);
returnValue = command2.ExecuteNonQuery();
writer.WriteLine("Rows to be affected by command2: {0}", returnValue);
}
}
// The Complete method commits the transaction. If an exception has been thrown,
// Complete is not called and the transaction is rolled back.
scope.Complete();
}
}
catch (TransactionAbortedException ex)
{
writer.WriteLine("TransactionAbortedException Message: {0}", ex.Message);
}
// Display messages.
Console.WriteLine(writer.ToString());
return returnValue;
}
' This function takes arguments for 2 connection strings and commands to create a transaction
' involving two SQL Servers. It returns a value > 0 if the transaction is committed, 0 if the
' transaction is rolled back. To test this code, you can connect to two different databases
' on the same server by altering the connection string, or to another 3rd party RDBMS
' by altering the code in the connection2 code block.
Public Function CreateTransactionScope( _
ByVal connectString1 As String, ByVal connectString2 As String, _
ByVal commandText1 As String, ByVal commandText2 As String) As Integer
' Initialize the return value to zero and create a StringWriter to display results.
Dim returnValue As Integer = 0
Dim writer As System.IO.StringWriter = New System.IO.StringWriter
Try
' Create the TransactionScope to execute the commands, guaranteeing
' that both commands can commit or roll back as a single unit of work.
Using scope As New TransactionScope()
Using connection1 As New SqlConnection(connectString1)
' Opening the connection automatically enlists it in the
' TransactionScope as a lightweight transaction.
connection1.Open()
' Create the SqlCommand object and execute the first command.
Dim command1 As SqlCommand = New SqlCommand(commandText1, connection1)
returnValue = command1.ExecuteNonQuery()
writer.WriteLine("Rows to be affected by command1: {0}", returnValue)
' If you get here, this means that command1 succeeded. By nesting
' the using block for connection2 inside that of connection1, you
' conserve server and network resources as connection2 is opened
' only when there is a chance that the transaction can commit.
Using connection2 As New SqlConnection(connectString2)
' The transaction is escalated to a full distributed
' transaction when connection2 is opened.
connection2.Open()
' Execute the second command in the second database.
returnValue = 0
Dim command2 As SqlCommand = New SqlCommand(commandText2, connection2)
returnValue = command2.ExecuteNonQuery()
writer.WriteLine("Rows to be affected by command2: {0}", returnValue)
End Using
End Using
' The Complete method commits the transaction. If an exception has been thrown,
' Complete is called and the transaction is rolled back.
scope.Complete()
End Using
Catch ex As TransactionAbortedException
writer.WriteLine("TransactionAbortedException Message: {0}", ex.Message)
End Try
' Display messages.
Console.WriteLine(writer.ToString())
Return returnValue
End Function
Der Transaktionsbereich wird begonnen, sobald ein neues TransactionScope-Objekt erstellt wird. Es wird empfohlen, Bereiche wie im Codebeispiel veranschaulicht mit einer using
-Anweisung zu erstellen. Die using
-Anweisung ist sowohl in C# als auch in Visual Basic verfügbar und funktioniert wie ein try
...finally
-Block, um sicherzustellen, dass der Block ordnungsgemäß freigegeben wird.
Beim Instanziieren von TransactionScope bestimmt der Transaktions-Manager, an welcher Transaktion der Bereich beteiligt sein soll. Sobald er festgelegt wurde, ist der Bereich immer an dieser Transaktion beteiligt. Die Entscheidung hängt von zwei Faktoren ab: vom Vorhandensein einer umgebenden Transaktion und vom Wert des TransactionScopeOption
-Parameters im Konstruktor. Die Ambient-Transaktion ist die Transaktion, in der der Code ausgeführt wird. Ein Verweis auf die Ambient-Transaktion kann durch einen Aufruf der statischen Transaction.Current-Eigenschaft der Transaction-Klasse abgerufen werden. Weitere Informationen zur Verwendung dieses Parameters finden Sie im Abschnitt Verwalten eines Transaktionsflusses mit TransactionScopeOption in diesem Thema.
Vervollständigen eines Transaktionsbereichs
Nachdem die Anwendung alle in einer Transaktion auszuführenden Arbeiten abgeschlossen hat, sollten Sie die TransactionScope.Complete-Methode nur einmal aufrufen, um den Transaktions-Manager darüber zu benachrichtigen, dass für die Transaktion ein Commit ausgeführt werden kann. Der Aufruf von Complete wird üblicherweise als letzte Anweisung in den using
-Block eingefügt.
Wenn diese Methode nicht aufgerufen werden kann, wird die Transaktion abgebrochen, weil dies vom Transaktions-Manager als Systemfehler oder wie eine im Transaktionsbereich ausgelöste Ausnahme interpretiert wird. Ein Aufruf dieser Methode garantiert aber nicht, dass ein Commit für die Transaktion ausgeführt wird. Dies ist nur eine Möglichkeit, den Transaktions-Manager über den Status zu informieren. Nach dem Aufruf der Complete-Methode können Sie nicht mehr über die Current-Eigenschaft auf die Ambient-Transaktion zugreifen. Wenn Sie dies dennoch versuchen, wird eine Ausnahme ausgelöst.
Wenn das TransactionScope-Objekt die Transaktion ursprünglich erstellt hat, erfolgt der eigentliche Commit der Transaktion durch den Transaktions-Manager nach der letzten Codezeile im using
-Block. Wenn die Transaktion nicht erstellt wurde, wird der Commit ausgeführt, wenn Commit vom Besitzer des CommittableTransaction-Objekts aufgerufen wird. Nun ruft der Transaktions-Manager die Ressourcen-Manager auf und weist sie an, entweder einen Commit oder ein Rollback auszuführen, je nachdem, ob die Complete-Methode für das TransactionScope-Objekt aufgerufen wurde.
Mit der using
-Anweisung wird sichergestellt, dass die Dispose-Methode des TransactionScope-Objekts auch dann aufgerufen wird, wenn eine Ausnahme auftritt. Der Aufruf der Dispose-Methode kennzeichnet das Ende des Transaktionsbereichs. Ausnahmen, die nach dem Aufrufen dieser Methode eintreten, beeinflussen die Transaktion möglicherweise nicht. Diese Methode stellt auch den vorherigen Zustand der Ambient-Transaktion wieder her.
Eine TransactionAbortedException wird ausgelöst, wenn eine vom Transaktionsbereich erstellte Transaktion abgebrochen wird. Eine TransactionInDoubtException wird ausgelöst, wenn der Transaktions-Manager nicht entscheiden kann, ob ein Commit ausgeführt werden soll. Wenn ein Commit für die Transaktion ausgeführt wird, wird keine Ausnahme ausgelöst.
Ausführen eines Rollbacks für eine Transaktion
Wenn ein Rollback für eine Transaktion ausgeführt werden soll, dürfen Sie die Complete-Methode nicht im Transaktionsbereich aufrufen. Zum Beispiel können Sie eine Ausnahme im Bereich auslösen. Der Rollback wird für die Transaktion ausgeführt, die im Bereich liegt.
Verwalten eines Transaktionsflusses mit TransactionScopeOption
Transaktionsbereiche können geschachtelt werden, indem eine Methode aufgerufen wird, die ein TransactionScope-Objekt innerhalb einer Methode verwendet, die ihren eigenen Bereich verwendet. Die RootMethod
-Methode im folgenden Beispiel veranschaulicht dies.
void RootMethod()
{
using(TransactionScope scope = new TransactionScope())
{
/* Perform transactional work here */
SomeMethod();
scope.Complete();
}
}
void SomeMethod()
{
using(TransactionScope scope = new TransactionScope())
{
/* Perform transactional work here */
scope.Complete();
}
}
Der oberste Transaktionsbereich wird als Stammbereich bezeichnet.
Die TransactionScope-Klasse enthält einige überladene Konstruktoren, die eine Enumeration vom Typ TransactionScopeOption akzeptieren, die das Transaktionsverhalten des Bereichs definiert.
Ein TransactionScope-Objekt verfügt über drei Optionen:
Verknüpfen der Ambient-Transaktion oder Erstellen einer neuen Transaktion, wenn keine vorhanden ist.
Definieren eines neuen Stammbereichs, d. h. Starten einer neuen Transaktion und Festlegen dieser Transaktion als neue Ambient-Transaktion in ihrem eigenem Bereich.
Nicht teilnehmen an einer Transaktion. Daraus resultiert keine Ambient-Transaktion.
Wenn der Bereich mit Required instanziiert wird und eine Ambient-Transaktion vorhanden ist, erstellt der Bereich eine Verknüpfung mit dieser Transaktion. Ist keine Ambient-Transaktion vorhanden, erstellt der Bereich eine neue Transaktion und wird zum Stammbereich. Dies ist der Standardwert. Wenn Required verwendet wird, muss der Code innerhalb des Bereichs kein anderes Verhalten zeigen, gleichgültig, ob es sich um den Stammbereich oder nur um eine Verknüpfung mit der Ambient-Transaktion handelt. Er sollte in beiden Fällen das Gleiche ausführen.
Wenn der Bereich mit RequiresNew instanziiert wird, ist er immer der Stammbereich. Er startet eine neue Transaktion, und seine Transaktion wird zur neuen Ambient-Transaktion im Bereich.
Wird der Bereich mit Suppress instanziiert, dann ist er nie an einer Transaktion beteiligt, unabhängig davon, ob eine Ambient-Transaktion vorhanden ist. Für einen mit diesem Wert instanziierten Bereich ist als Ambient-Transaktion immer null
festgelegt.
Die obigen Optionen sind in der folgenden Tabelle zusammengefasst.
TransactionScopeOption | Ambient-Transaktion | Der Bereich ist beteiligt an |
---|---|---|
Erforderlich | Nein | Neue Transaktion (wird zum Stamm) |
Requires New | Nein | Neue Transaktion (wird zum Stamm) |
Suppress | Nein | Keine Transaktion |
Erforderlich | Ja | Ambient-Transaktion |
Requires New | Ja | Neue Transaktion (wird zum Stamm) |
Suppress | Ja | Keine Transaktion |
Wenn ein TransactionScope-Objekt eine Verknüpfung mit einer vorhandenen Ambient-Transaktion erstellt, wird die Transaktion unter Umständen nur dann durch die Freigabe des Bereichsobjekts beendet, wenn der Bereich die Transaktion abbricht. Wenn die Ambient-Transaktion vom Stammbereich erstellt wurde, wird die Commit-Methode nur dann für die Transaktion aufgerufen, wenn der Stammbereich gelöscht wird. Wurde die Transaktion manuell erstellt, dann endet die Transaktion, wenn sie abgebrochen oder von ihrem Ersteller die Commit-Methode aufgerufen wird.
Im folgenden Beispiel werden mit einem TransactionScope-Objekt drei verschachtelte Bereichsobjekte erstellt, die jeweils mit einem anderen TransactionScopeOption-Wert instanziiert werden.
using(TransactionScope scope1 = new TransactionScope())
//Default is Required
{
using(TransactionScope scope2 = new TransactionScope(TransactionScopeOption.Required))
{
//...
}
using(TransactionScope scope3 = new TransactionScope(TransactionScopeOption.RequiresNew))
{
//...
}
using(TransactionScope scope4 = new TransactionScope(TransactionScopeOption.Suppress))
{
//...
}
}
Im Beispiel wird ein Codeblock ohne Ambient-Transaktion gezeigt, in dem ein neuer Bereich (scope1
) mit Required erstellt wird. Der Bereich scope1
ist ein Stammbereich, da er eine neue Transaktion (Transaction A) erstellt und Transaction A als Ambient-Transaktion definiert. Scope1
erstellt dann drei weitere Objekte mit jeweils einem anderen TransactionScopeOption-Wert. Beispielsweise wird scope2
mit Required erstellt, und da eine Ambient-Transaktion vorhanden ist, wird eine Verknüpfung mit der ersten von scope1
erstellten Transaktion erstellt. Beachten Sie, dass scope3
der Stammbereich der neuen Transaktion ist, und dass scope4
keine Ambient-Transaktion enthält.
Der am häufigsten verwendete Wert und Standardwert von TransactionScopeOption lautet zwar Required, aber auch die anderen Werte erfüllen jeweils einen bestimmten Zweck.
Nicht transaktionaler Code innerhalb eines Transaktionsbereichs
Suppress ist nützlich, wenn Sie die vom Codeabschnitt ausgeführten Vorgänge beibehalten und die Ambient-Transaktion nicht abbrechen möchten, wenn die Vorgänge fehlschlagen. Wenn beispielsweise Aktivitäten protokolliert oder Überwachungsoperationen ausgeführt werden sollen oder wenn Ereignisse für Abonnenten veröffentlicht werden sollen, unabhängig davon, ob die Ambient-Transaktion abgeschlossen oder abgebrochen wird. Dieser Wert ermöglicht es, wie im folgenden Beispiel gezeigt, in einen Transaktionsbereich einen von der Transaktion unabhängigen Codeabschnitt einzufügen.
using(TransactionScope scope1 = new TransactionScope())
{
try
{
//Start of non-transactional section
using(TransactionScope scope2 = new
TransactionScope(TransactionScopeOption.Suppress))
{
//Do non-transactional work here
}
//Restores ambient transaction here
}
catch {}
//Rest of scope1
}
Abstimmen in einem geschachtelten Bereich
Obwohl ein geschachtelter Bereich eine Verknüpfung mit der Ambient-Transaktion des Stammbereichs erstellen kann, wirkt sich ein Aufruf von Complete im geschachtelten Bereich nicht auf den Stammbereich aus. Die Transaktion wird nur dann abgeschlossen, wenn alle Bereiche, vom Stammbereich bis zum letzten geschachtelten Bereich, dem Commit der Transaktion zustimmen. Wenn Complete nicht in einem geschachtelten Bereich aufgerufen wird, wirkt sich dies auf den Stammbereich aus, da die Ambient-Transaktion sofort abgebrochen wird.
Festlegen des TransactionScope-Timeouts
Einige der überladenen Konstruktoren von TransactionScope akzeptieren einen Wert vom Typ TimeSpan, der zur Steuerung des Timeouts einer Transaktion dient. Ein Timeout mit dem Wert Null (0) steht für ein unendliches Timeout. Ein unendliches Timeout ist vor allem beim Debuggen hilfreich, wenn ein Problem in der Geschäftslogik durch das schrittweise Ausführen des Codes eingegrenzt werden soll, und wenn während der Suche nach der Problemursache bei der Transaktion, die untersucht wird, kein Timeout auftreten soll. Verwenden Sie den unendlichen Timeoutwert in allen anderen Fällen mit äußerster Vorsicht, weil dadurch die Sicherungsmechanismen gegen Transaktions-Deadlocks außer Kraft gesetzt werden.
Sie legen den TransactionScope-Timeout i. d. R. in zwei Fällen auf einen anderen Wert als den Standardwert fest. Erstens während der Entwicklung, wenn Sie testen möchten, wie eine Anwendung mit abgebrochenen Transaktionen umgeht. Wenn Sie für den Timeout einen kleinen Wert (z. B. eine Millisekunde) angeben, kann dies zum Fehlschlagen der Transaktion führen, und Sie können folglich den Fehlerbehandlungscode überprüfen. Zweitens wählen Sie einen kleineren Wert als den Standardtimeoutwert, wenn Sie annehmen, dass der Bereich zu einem Ressourcenkonflikt beiträgt, der zu Deadlocks führt. In diesem Fall soll die Transaktion so bald wie möglich abgebrochen werden und nicht auf den Ablauf des Standardtimeouts warten.
Wenn in einem Bereich eine Verknüpfung mit einer Ambient-Transaktion hergestellt wird, aber ein kleinerer Timeoutwert als der für die Ambient-Transaktion definierte Wert angegeben wird, dann wird der neue kürzere Timeoutwert für das TransactionScope-Objekt übernommen. Wird der Bereich hier nicht innerhalb der in der Ambient-Transaktion angegebenen Zeitspanne beendet, wird die Transaktion automatisch beendet. Wenn der Timeoutwert des geschachtelten Bereichs größer ist als der der Ambient-Transaktion, hat er keine Auswirkungen.
Festlegen der TransactionScope-Isolationsstufe
Einige der überladenen Konstruktoren von TransactionScope akzeptieren neben dem Timeoutwert eine Struktur vom Typ TransactionOptions zur Festlegung der Isolationsstufe. Standardmäßig wird zur Ausführung einer Transaktion die Isolationsstufe auf Serializable festgelegt. Häufig wird für Systeme, die viele Lesevorgänge ausführen müssen, eine andere Isolationsstufe als Serializable gewählt. Dies erfordert ein umfassendes Verständnis der Transaktionsverarbeitungstheorie und der Semantik der betreffenden Transaktion, der einschlägigen Parallelitätsprobleme und der Auswirkungen auf die Systemkonsistenz.
Überdies unterstützen nicht alle Ressourcen-Manager alle Isolationsstufen, sodass ein Ressourcen-Manager die Transaktion möglicherweise auf einer höheren als der konfigurierten Isolationsstufe bearbeitet.
Jede Isolationsstufe außer Serializable ist für Inkonsistenzen anfällig, die sich daraus ergeben können, dass andere Transaktionen auf die gleichen Informationen zugreifen. Die verschiedenen Isolationsstufen unterscheiden sich darin, wie Lese- und Schreibsperren verwendet werden. Eine Sperre kann nur dann gelten, wenn die Transaktion im Ressourcen-Manager auf Daten zugreift. Sie kann aber solange gelten, bis die Transaktion abgeschlossen oder abgebrochen wurde. Im ersten Fall ist der Durchsatz höher, im zweiten Fall die Konsistenz. Da es zwei Arten von Sperren und zwei Arten von Operationen (Lesen/Schreiben) gibt, sind vier grundlegende Isolationsstufen verfügbar. Weitere Informationen finden Sie unter IsolationLevel.
Beim Einsatz verschachtelter TransactionScope-Objekte muss für alle verschachtelten Bereiche die gleiche Isolationsstufe konfiguriert werden, wenn eine Verknüpfung mit der Ambient-Transaktion hergestellt werden soll Wenn verschachtelte TransactionScope-Objekte eine Verknüpfung mit der Ambient-Transaktion herzustellen versuchen und für diese eine andere Isolationsstufe festgelegt wurde, dann wird eine Ausnahme des Typs ArgumentException ausgelöst.
Zusammenarbeit mit COM+
Wenn Sie eine neue TransactionScope-Instanz erstellen, können Sie die EnterpriseServicesInteropOption-Enumeration in einem der Konstruktoren verwenden, um die Zusammenarbeit mit COM+ näher zu bestimmen. Weitere Informationen hierzu finden Sie unter Interoperabilität mit Enterprise Services und COM+-Transaktionen.