Dela via


Implementera en implicit transaktion med transaktionsomfång

Klassen TransactionScope är ett enkelt sätt att markera ett kodblock som deltagande i en transaktion, utan att du behöver interagera med själva transaktionen. Ett transaktionsomfång kan automatiskt välja och hantera den omgivande transaktionen. På grund av dess användarvänlighet och effektivitet rekommenderar vi att du använder TransactionScope klassen när du utvecklar ett transaktionsprogram.

Dessutom behöver du inte uttryckligen registrera resurser med transaktionen. Alla System.Transactions resurshanterare (till exempel SQL Server 2005) kan upptäcka förekomsten av en aktiv transaktion som skapats av omfånget och automatiskt ansluta sig.

Skapa ett transaktionsomfång

Följande exempel visar en enkel användning av TransactionScope klassen.

// 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

Transaktionsomfånget startas när du har skapat ett nytt TransactionScope objekt. Som du ser i kodexemplet rekommenderar vi att du skapar omfång med en using -instruktion. -instruktionen using är tillgänglig både i C# och i Visual Basic och fungerar som ett try...finally -block för att säkerställa att omfånget tas bort korrekt.

När du instansierar TransactionScopeavgör transaktionshanteraren vilken transaktion som ska ingå. När detta omfång fastställts deltar det alltid i den transaktionen. Beslutet baseras på två faktorer: om det finns en omgivande transaktion och värdet för parametern TransactionScopeOption i konstruktorn. Den omgivande transaktionen är den transaktion där din kod körs. Du kan hämta en referens till den omgivande transaktionen genom att anropa klassens Transaction.Current statiska Transaction egenskap. Mer information om hur den här parametern används finns i avsnittet Hantera transaktionsflöde med TransactionScopeOption i det här avsnittet.

Slutföra ett transaktionsomfång

När programmet har slutfört allt arbete som det vill utföra i en transaktion bör du bara anropa TransactionScope.Complete metoden en gång för att informera transaktionshanteraren om att det är acceptabelt att genomföra transaktionen. Det är mycket bra praxis att placera anropet till Complete som den sista instruktionen i using-blocket.

Om du inte anropar den här metoden avbryts transaktionen eftersom transaktionshanteraren tolkar detta som ett systemfel eller motsvarar ett undantag som genereras inom transaktionens omfång. Att anropa den här metoden garanterar dock inte att transaktionen kommer att slutföras. Det är bara ett sätt att informera transaktionshanteraren om din status. När du har anropat Complete metoden kan du inte längre komma åt den omgivande transaktionen med hjälp av Current egenskapen, och om du försöker göra det genereras ett fel.

TransactionScope Om objektet skapade transaktionen från början sker det faktiska arbetet med att genomföra transaktionen av transaktionshanteraren efter den sista kodraden using i blocket. Om den inte skapade transaktionen bekräftas det när Commit anropas av ägaren av CommittableTransaction-objektet. Då anropar transaktionshanteraren resurshanterarna och informerar dem om att antingen checka in eller återställa, baserat på om Complete metoden anropades för TransactionScope objektet.

Instruktionen using säkerställer att metoden Dispose för objektet TransactionScope anropas även om ett undantag inträffar. Metoden Dispose markerar slutet på transaktionsomfånget. Undantag som inträffar efter att den här metoden anropats kanske inte påverkar transaktionen. Den här metoden återställer även den omgivande transaktionen till det tidigare tillståndet.

En TransactionAbortedException kastas om omfånget skapar transaktionen, och transaktionen avbryts. En TransactionInDoubtException kastas om transaktionshanteraren inte kan fatta ett Commit-beslut. Inget undantag kastas om transaktionen har genomförts.

Återställa en transaktion

Om du vill återställa en transaktion bör du inte anropa Complete metoden inom transaktionsomfånget. Du kan till exempel utlösa ett undantag inom omfånget. Transaktionen som den deltar i kommer att återställas.

Hantera transaktionsflöde med TransactionScopeOption

Transaktionsomfång kan kapslas genom att anropa en metod som använder en TransactionScope inifrån en metod som använder sitt eget omfång, vilket är fallet med RootMethod metoden i följande exempel,

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();
    }
}

Det översta transaktionsomfånget kallas rotomfånget.

Klassen TransactionScope innehåller flera överlagrade konstruktorer som accepterar en uppräkning av typen TransactionScopeOption, som definierar transaktionsbeteendet för omfånget.

Ett TransactionScope objekt har tre alternativ:

  • Anslut den omgivande transaktionen eller skapa en ny om den inte finns.

  • Vara ett nytt rotomfång, det vill ex. starta en ny transaktion och låta den transaktionen vara den nya omgivande transaktionen inom sitt eget omfång.

  • Delta inte i en transaktion alls. Det finns ingen omgivande transaktionsmiljö som resultat.

Om omfånget instansieras med Required och en omgivande transaktion finns, ansluter omfånget till den transaktionen. Om det å andra sidan inte finns någon omgivande transaktion skapar omfånget en ny transaktion och blir rotomfånget. Det här är standardvärdet. När Required används behöver koden i omfånget inte bete sig annorlunda oavsett om det är roten eller bara ansluter till den omgivande transaktionen. Den bör fungera identiskt i båda fallen.

Om omfånget instansieras med RequiresNew, är det alltid rotomfånget. Den startar en ny transaktion, och denna blir den nya omgivande transaktionen inom omfånget.

Om omfånget instansieras med Suppressdeltar det aldrig i en transaktion, oavsett om det finns en omgivande transaktion. Ett omfång som instansieras med det här värdet har alltid null som sin omgivande transaktion.

Alternativen ovan sammanfattas i följande tabell.

TransactionScopeOption Miljötransaktion Omfattningen ingår i
Krävs Nej Ny transaktion (kommer att vara huvudtransaktionen)
Kräver något nytt Nej Ny transaktion (kommer att vara huvudtransaktionen)
Undertrycka Nej Ingen transaktion
Krävs Ja Miljötransaktion
Kräver något nytt Ja Ny transaktion (kommer att vara huvudtransaktionen)
Undertrycka Ja Ingen transaktion

När ett TransactionScope objekt ansluter till en befintlig omgivande transaktion kan det hända att omfångsobjektet inte avslutar transaktionen, såvida inte omfånget avbryter transaktionen. Om den omgivande transaktionen skapades av ett rotomfång anropas Commit på transaktionen endast när rotomfånget tas bort. Om transaktionen skapades manuellt, avslutas transaktionen när den antingen avbryts, eller slutförs av skaparen.

I följande exempel visas ett TransactionScope objekt som skapar tre kapslade omfångsobjekt, var och en instansierad med ett annat TransactionScopeOption värde.

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))
    {
        //...  
    }
}

Exemplet visar ett kodblock utan någon omgivande transaktion som skapar ett nytt omfång (scope1) med Required. Omfånget scope1 är ett rotomfång eftersom det skapar en ny transaktion (transaktion A) och gör Transaktion A till den omgivande transaktionen. Scope1 skapar sedan ytterligare tre objekt, var och en med ett annat TransactionScopeOption värde. Till exempel scope2 skapas med Required, och eftersom det finns en omgivande transaktion ansluter den den första transaktionen som skapats av scope1. Observera att scope3 är rotomfånget för en ny transaktion och att scope4 inte har någon omgivande transaktion.

Även om standardvärdet och det vanligaste värdet TransactionScopeOption för är Requiredhar vart och ett av de andra värdena sitt unika syfte.

Icke-transaktionskod inom ett transaktionsområde

Suppress är användbart när du vill bevara de åtgärder som utförs av kodavsnittet och inte vill avbryta den omgivande transaktionen om åtgärderna misslyckas. Till exempel när du vill utföra loggnings- eller revisionsåtgärder, eller när du vill publicera händelser till prenumeranter oavsett om din omgivande transaktion genomförs eller återkallas. Med det här värdet kan du ha ett icke-transaktionellt kodavsnitt i ett transaktionsomfång, som du ser i följande exempel.

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
}

Röstning i ett nestat område

Även om ett kapslat område kan ansluta till den övergripande transaktionen i rotområdet har anrop Complete i det kapslade området ingen effekt på rotområdet. Transaktionen kommer endast att genomföras om alla omfång, uppifrån och ned till det sista kapslade omfånget, röstar för att godkänna transaktionen. Om du inte anropar Complete i ett nästlat omfång påverkas grundomfånget eftersom den pågående transaktionen omedelbart avbryts.

Ange tidsgränsen för TransactionScope

Vissa av de överbelastade konstruktorerna hos TransactionScope accepterar ett värde av typen TimeSpan, som används för att styra tidsgränsen för transaktionen. En timeout inställd på noll innebär en oändlig timeout. Oändlig timeout är användbart främst för felsökning, när du vill isolera ett problem i affärslogiken genom att gå igenom koden, och du inte vill att den transaktion som du felsöker ska överskrida tidsgränsen när du försöker hitta problemet. Var mycket försiktig med att använda det oändliga timeout-värdet i alla andra fall, eftersom det åsidosätter skyddet mot transaktionslåsningar.

Du brukar ställa in timeout till andra värden än standard i två fall. Den första är under utvecklingen, när du vill testa hur programmet hanterar avbrutna transaktioner. Genom att ange timeouten till ett litet värde (till exempel en millisekunder) får du transaktionen att misslyckas och kan därför observera felhanteringskoden. Det andra fallet där du anger att värdet ska vara mindre än standardtimeouten är när du tror att omfånget är involverat i resurskonkurrering, vilket resulterar i dödlägen. I så fall vill du avbryta transaktionen så snart som möjligt och inte vänta tills standardtimeouten upphör att gälla.

När ett omfång ansluter till en omgivande transaktion men anger en mindre timeout än den som den omgivande transaktionen är inställd på tillämpas den nya, kortare tidsgränsen TransactionScope på objektet och omfånget måste avslutas inom den angivna kapslade tiden eller så avbryts transaktionen automatiskt. Om tidsgränsen för det kapslade omfånget är längre än den för den omgivande transaktionen, har den ingen effekt.

Ange transactionscope-isoleringsnivån

Vissa av de överlagrade konstruktorerna TransactionScope accepterar en struktur av typen TransactionOptions för att ange en isoleringsnivå, utöver ett timeout-värde. Som standard körs transaktionen med isoleringsnivån inställd på Serializable. Att välja en annan isoleringsnivå än Serializable används ofta för läsintensiva system. Detta kräver en gedigen förståelse för transaktionsbearbetningsteorin och semantiken för själva transaktionen, samtidighetsproblemen och konsekvenserna för systemkonsekvensen.

Dessutom stöder inte alla resurshanterare alla isoleringsnivåer, och de kan välja att delta i transaktionen på en högre nivå än den som konfigurerats.

Varje isoleringsnivå förutom Serializable är känslig för inkonsekvens till följd av andra transaktioner som har åtkomst till samma information. Skillnaden mellan de olika isoleringsnivåerna är hur läs- och skrivlås används. Ett lås kan hållas endast när transaktionen når data i resurshanteraren, eller så kan det behållas tills transaktionen har bekräftats eller nekats. Det förstnämnda är bättre för dataflöde, det senare för konsekvens. De två typerna av lås och de två typerna av åtgärder (läsning/skrivning) ger fyra grundläggande isoleringsnivåer. Mer information finns i IsolationLevel.

När du använder kapslade TransactionScope objekt måste alla kapslade omfång konfigureras för att använda exakt samma isoleringsnivå om de vill ansluta till den omgivande transaktionen. Om ett kapslat TransactionScope-objekt försöker delta i den omgivande transaktionen men som specifiserar en annan isoleringsnivå, kastas en ArgumentException.

Interop med COM+

När du skapar en ny TransactionScope instans kan du använda EnterpriseServicesInteropOption uppräkningen i någon av konstruktorerna för att ange hur du ska interagera med COM+. Mer information om detta finns i Samverkan med Enterprise Services och COM+-transaktioner.

Se även