2010-02-02 12 views
6

Estoy tratando de determinar el comportamiento de la conexión de bases de datos múltiples en una transacción distribuida.¿Cómo se comportan las transacciones distribuidas con múltiples conexiones al mismo DB en un entorno enhebrado?

Tengo un proceso de larga ejecución que genera una serie de subprocesos y cada subproceso es responsable de administrar sus 'conexiones de base de datos y tal. Todo esto se ejecuta dentro del alcance de la transacción y cada hilo se alista en la transacción a través de un objeto DependentTransaction.

Cuando fui a poner este proceso en paralelo me encontré con algunos problemas, es decir, que parece haber algún tipo de bloqueo que impide que las consultas se ejecuten al mismo tiempo en la transacción.

Lo que me gustaría saber es cómo el coordinador de transacciones maneja las consultas de múltiples conexiones a la misma base de datos y si incluso es aconsejable pasar un objeto de conexión a través de subprocesos?

He leído que MS SQL solo permite una conexión por transacción, pero claramente puedo crear e inicializar más de una conexión al mismo DB en la misma transacción. Simplemente no puedo ejecutar los subprocesos en paralelo sin obtener una excepción de "Contexto de transacción en uso por otra sesión" al abrir las conexiones. El resultado es que las conexiones tienen que esperar para ejecutarse en lugar de ejecutarse al mismo tiempo y, al final, el código se ejecuta hasta su finalización, pero no hay ganancia neta para enhebrar la aplicación debido a este problema de bloqueo.

El código se parece a esto.

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 

EDITAR

Mis pruebas han demostrado de manera concluyente que esto no se puede hacer. Incluso si hay más de una conexión o se utiliza la misma conexión, todas las solicitudes en la transacción o las preguntas se procesan secuencialmente.

Quizás cambien este comportamiento en el futuro.

+0

OK veo que tiene un dilema. Pero desde la perspectiva de SQL, una transacción debe tener límites. En mi opinión, respetar una transacción a través de múltiples conexiones viola el concepto. Puedo pedir mi ensalada con mi cena, pero no quiero que la siguiente mesa diga que quieren su ensalada con la mía. – Paparazzi

Respuesta

8

En primer lugar, debe separar lo que lee aquí y allá sobre las transacciones de SQL Server en dos casos distintos: local y distribuido.

transacciones SQL locales:

  • SQL Server permite sólo una solicitud para ejecutar en cada transacción local.
  • De forma predeterminada, solo una sesión puede inscribirse en una transacción local. Al usar sp_getbindtoken y sp_bindsession, se pueden inscribir múltiples sesiones en una transacción local. Las sesiones todavía están restringidas a solo una que ejecuta una solicitud en cualquier momento.
  • Con múltiples conjuntos de resultados activos (MARS), una sesión puede ejecutar varias solicitudes. Todas las solicitudes deben estar inscritas en la misma transacción local.

transacciones distribuidas:

  • múltiples sesiones pueden tener su transacción local inscrito en una sola transacción distribuida.
  • Cada sesión sigue inscribió en una transacción local, sujeto a todas las restricciones mencionadas anteriormente para las transacciones locales
  • transacciones locales inscritos a una transacción distribuida están sujetas a confirmación en dos fases coordinado por la transacción distribuida
  • Todas las transacciones locales una instancia inscrita en una transacción distribuida sigue siendo transacciones locales independientes, lo que significa principalmente que tienen espacios de nombres de bloqueo conflictivos.

De modo que cuando un cliente crea .Net TransactionScope y bajo este ámbito de transacción ejecuta múltiples solicitudes en el mismo servidor, estas solicitudes son todas transacciones locales inscritas en una transacción distribuida. Un ejemplo sencillo:

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

Crear una tabla de prueba simulado:

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

y ejecutar el código en mi muestra. Verá que ambas solicitudes se están ejecutando en paralelo, cada una está haciendo 100k filas en la tabla, y luego ambas se comprometerán cuando se complete el alcance de la transacción. Entonces, los problemas que está viendo no están relacionados con SQL Server ni con TransactionScope, pueden manejar fácilmente el escenario que describe. Además, el código es muy simple y sencillo, y no es necesario crear transacciones dependientes, clonar para que ocurra ni transacciones que promocionar.

Actualizado

El uso de hilos explícitas y transacciones dependientes:

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

Gracias por la respuesta. ¿Cómo harías esto con múltiples hilos? Tengo que poder ejecutar varios subprocesos en mi escenario para que la transacción se distribuya entre ellos. En función de su publicación, parece que tendré que pasar el objeto de conexión a través de los hilos. ¿Es eso una evaluación correcta de esto entonces? – Middletone

+0

Ver mi actualización. No pude obtener la transcation dependiente para abrir la conexión en el hilo, tuve que pasar la conexión en abierto (y supongo que ya se enlistó en el DTC). –

+0

Mis pruebas han demostrado de manera concluyente que esto simplemente no se puede hacer. Incluso si hay más de una conexión o se utiliza la misma conexión, todas las solicitudes en la transacción o las preguntas se procesan secuencialmente. Quizás cambien este comportamiento en el futuro. – Middletone

Cuestiones relacionadas