2010-02-02 8 views
6

Sto cercando di determinare il comportamento di più connessioni di database in una transazione distribuita.Come si comportano le transazioni distribuite con più connessioni allo stesso DB in un ambiente con thread?

Ho un lungo processo in esecuzione che genera una serie di thread e ciascun thread è quindi responsabile della gestione delle sue connessioni DB e così via. Tutto questo viene eseguito all'interno dell'ambito della transazione e ogni thread viene inserito nella transazione tramite un oggetto DependentTransaction.

Quando sono andato a mettere questo processo in parallelo ho incontrato alcuni problemi, vale a dire che sembra esserci una specie di blocco che impedisce l'esecuzione delle query contemporaneamente alla transazione.

Quello che vorrei sapere è come il coordinatore delle transazioni gestisce le query da più connessioni allo stesso DB e se è addirittura consigliabile passare un oggetto di connessione tra thread?

Ho letto che MS SQL consente solo una connessione per transazione, ma sono chiaramente in grado di creare e inizializzare più di una connessione allo stesso DB nella stessa transazione. Semplicemente non sono in grado di eseguire i thread in parallelo senza ottenere un'eccezione "Contesto transazioni in uso da un'altra sessione" quando si aprono le connessioni. Il risultato è che le connessioni devono attendere per essere eseguite anziché funzionare allo stesso tempo e alla fine il codice viene eseguito fino al completamento, ma non vi è alcun guadagno netto nel threading dell'app a causa di questo problema di blocco.

Il codice è simile a questo.

Sub StartThreads() 
     Using Scope As New TransactionScope 
      Dim TL(100) As Tasks.Task 
      Dim dTx As DependentTransaction 
      For i As Int32 = 0 To 100 
       Dim A(1) As Object 
       dTx = CType(Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete), DependentTransaction) 
       'A(0) = some_other_data 
       A(1) = dTx 'the Dependent Transaction 

       TL(i) = Tasks.Task.Factory.StartNew(AddressOf Me.ProcessData, A) 'Start the thread and add it to the array 
      Next 

      Tasks.Task.WaitAll(TL) 'Wait for threads to finish 

      Scope.Complete() 
     End Using 
    End Sub 
    Dim TransLock As New Object 
    Sub ProcessData(ByVal A As Object) 
     Dim DTX As DependentTransaction = A(1) 
     Dim Trans As Transactions.TransactionScope 
     Dim I As Int32 
     Do While True 
      Try 
       SyncLock (TransLock) 
        Trans = New Transactions.TransactionScope(DTX, TimeSpan.FromMinutes(1)) 
       End SyncLock 
       Exit Do 
      Catch ex As TransactionAbortedException 
       If ex.ToString.Contains("Failure while attempting to promote transaction") Then 
       ElseIf ex.Message = "The transaction has aborted." Then 
        Throw New Exception(ex.ToString) 
        Exit Sub 
       End If 
       I += 1 
       If I > 5 Then 
        Throw New Exception(ex.ToString) 
       End If 
      Catch ex As Exception 

      End Try 
      Thread.Sleep(10) 
     Loop 
     Using Trans 
      Using DALS As New DAC.DALScope 
       Do While True 
        Try 
         SyncLock (TransLock) 
          'This opens two connection to the same DB for later use. 
          DALS.CurrentDAL.OpenConnection(DAC.DAL.ConnectionList.FirstConnection) 
          DALS.CurrentDAL.OpenConnection(DAC.DAL.ConnectionList.SecondConnection) 
         End SyncLock 
         Exit Do 
        Catch ex As Exception 
         'This is usually where I find the bottleneck 
         '"Transaction context in use by another session" is the exception that I get 
         Thread.Sleep(100) 
        End Try 
       Loop 

       '***************** 
       'Do some work here 
       '***************** 

       Trans.Complete() 
      End Using 
     End Using 
     DTX.Complete() 
    End Sub 

EDIT

miei test hanno definitivamente dimostrato che questo proprio non si può fare. Anche se c'è più di una connessione o la stessa connessione viene utilizzata tutte le richieste nella transazione o le domande vengono elaborate in sequenza.

Forse cambieranno questo comportamento in futuro.

+0

OK Vedo che avete un dilemma. Ma dal punto di vista di SQL una transazione deve avere limiti. Onorare una transazione attraverso più connessioni nella mia mente viola il concetto. Posso chiedere la mia insalata con la mia cena, ma non voglio che il tavolo accanto dica che vogliono la loro insalata con la mia. – Paparazzi

risposta

8

In primo luogo, è necessario separare ciò che si legge qua e là sulle transazioni di SQL Server in 2 casi distinti: locale e distribuito.

transazioni SQL locali:

  • SQL Server consente solo una richiesta per l'esecuzione su ogni transazione locale.
  • Per impostazione predefinita, una sola sessione può essere registrata in una transazione locale. Utilizzando sp_getbindtoken e sp_bindsession è possibile registrare più sessioni in una transazione locale. Le sessioni sono ancora limitate a una sola esecuzione di una richiesta in qualsiasi momento.
  • Con più set di risultati attivi (MARS), una sessione può eseguire più richieste. Tutte le richieste devono essere registrate nella stessa transazione locale.

transazioni distribuite:

  • più sessioni possono avere la loro transazione locale iscrive in una singola transazione distribuita.
  • Ogni sessione è ancora iscrive in una transazione locale, soggetta a tutte le restrizioni di cui sopra per le transazioni locali
  • transazioni locali iscrive in una transazione distribuita sono oggetto di commit a due fasi coordinato dalla transazione distribuita
  • Tutte le transazioni locali su un'istanza iscritta in una transazione distribuita è ancora indipendente transazioni locali, principalmente nel senso che hanno spazi dei nomi di blocco in conflitto.

Così quando un client crea un TransactionScope .Net e nell'ambito di questa transazione esegue più richieste sullo stesso server, queste richieste sono tutte transazioni locali registrate in una transazione distribuita. Un semplice esempio:

class Program 
    { 
     static string sqlBatch = @" 
set nocount on; 
declare @i int; 
set @i = 0; 
while @i < 100000 
begin 
    insert into test (a) values (replicate('a',100)); 
    set @i = @i+1; 
end"; 

     static void Main(string[] args) 
     { 
      try 
      { 
       TransactionOptions to = new TransactionOptions(); 
       to.IsolationLevel = IsolationLevel.ReadCommitted; 
       using (TransactionScope scp = new TransactionScope(TransactionScopeOption.Required, to)) 
       { 
        using (SqlConnection connA = new SqlConnection(Settings.Default.connString)) 
        { 
         connA.Open(); 
         using (SqlConnection connB = new SqlConnection(Settings.Default.connString)) 
         { 
          connB.Open(); 

          SqlCommand cmdA = new SqlCommand(sqlBatch, connA); 
          SqlCommand cmdB = new SqlCommand(sqlBatch, connB); 

          IAsyncResult arA = cmdA.BeginExecuteNonQuery(); 
          IAsyncResult arB = cmdB.BeginExecuteNonQuery(); 

          WaitHandle.WaitAll(new WaitHandle[] { arA.AsyncWaitHandle, arB.AsyncWaitHandle }); 

          cmdA.EndExecuteNonQuery(arA); 
          cmdB.EndExecuteNonQuery(arB); 
         } 
        } 
        scp.Complete(); 
       } 
      } 
      catch (Exception e) 
      { 
       Console.Error.Write(e); 
      } 
     } 
    } 

Crea una tabella di test dummy:

create table test (id int not null identity(1,1) primary key, a varchar(100)); 

ed eseguire il codice nel mio esempio. Si vedrà che entrambe le richieste sono in esecuzione in parallelo, ognuna delle quali contiene 100k righe nella tabella, quindi entrambe eseguono il commit quando l'ambito della transazione è completo. Quindi i problemi che stai vedendo non sono legati a SQL Server, né a TransactionScope, possono facilmente gestire lo scenario che descrivi. Inoltre, il codice è molto semplice e diretto e non è necessario creare transazioni dipendenti, clonazione o transazioni da promuovere.

Aggiornato

Utilizzando le discussioni esplicite e transazioni dipendenti:

private class ThreadState 
    { 
     public DependentTransaction Transaction {get; set;} 
     public EventWaitHandle Done {get; set;} 
     public SqlConnection Connection { get; set; } 
    } 
    static void Main(string[] args) 
    { 
     try 
     { 
      TransactionOptions to = new TransactionOptions(); 
      to.IsolationLevel = IsolationLevel.ReadCommitted; 
      using (TransactionScope scp = new TransactionScope(TransactionScopeOption.Required, to)) 
      { 
       ThreadState stateA = new ThreadState 
       { 
        Transaction = Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete), 
        Done = new AutoResetEvent(false), 
        Connection = new SqlConnection(Settings.Default.connString), 
       }; 
       stateA.Connection.Open(); 
       ThreadState stateB = new ThreadState 
       { 
        Transaction = Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete), 
        Done = new AutoResetEvent(false), 
        Connection = new SqlConnection(Settings.Default.connString), 
       }; 
       stateB.Connection.Open(); 

       ThreadPool.QueueUserWorkItem(new WaitCallback(Worker), stateA); 
       ThreadPool.QueueUserWorkItem(new WaitCallback(Worker), stateB); 

       WaitHandle.WaitAll(new WaitHandle[] { stateA.Done, stateB.Done }); 

       scp.Complete(); 

       //TODO: dispose the open connections 
      } 

     } 
     catch (Exception e) 
     { 
      Console.Error.Write(e); 
     } 
    } 

    private static void Worker(object args) 
    { 
     Debug.Assert(args is ThreadState); 
     ThreadState state = (ThreadState) args; 
     try 
     { 
      using (TransactionScope scp = new TransactionScope(state.Transaction)) 
      { 
       SqlCommand cmd = new SqlCommand(sqlBatch, state.Connection); 
       cmd.ExecuteNonQuery(); 
       scp.Complete(); 
      } 
      state.Transaction.Complete(); 
     } 
     catch (Exception e) 
     { 
      Console.Error.WriteLine(e); 
      state.Transaction.Rollback(); 
     } 
     finally 
     { 
      state.Done.Set(); 
     } 

    } 
+0

Grazie per la risposta. Come faresti questo con più thread? Devo essere in grado di eseguire più thread nel mio scenario e quindi la transazione sarebbe distribuita tra di loro. In base al tuo post sembra che dovrò passare l'oggetto di connessione attraverso i thread. È una valutazione corretta di questo allora? – Middletone

+0

Vedere il mio aggiornamento. Non ero in grado di ottenere la trascrizione dipendente per aprire la connessione nel thread, ho dovuto passare la connessione in già aperto (e presumo già inserito nel DTC). –

+0

I miei test hanno dimostrato in modo conclusivo che questo non può essere fatto. Anche se c'è più di una connessione o la stessa connessione viene utilizzata tutte le richieste nella transazione o le domande vengono elaborate in sequenza. Forse cambieranno questo comportamento in futuro. – Middletone