2010-11-13 20 views
10

¡Usar el objeto TransactionScope para configurar una transacción implícita que no necesita pasar a través de llamadas a funciones es excelente! Sin embargo, si se abre una conexión mientras que otra ya está abierta, el coordinador de transacciones escalará silenciosamente la transacción que se distribuirá (necesita que el servicio MSDTC se ejecute y requiera mucho más recursos y tiempo).Práctica recomendada para detener transacciones que se extienden a distribuidas al usar transactionscope

Por lo tanto, esto está muy bien:

 using (var ts = new TransactionScope()) 
     { 
      using (var c = DatabaseManager.GetOpenConnection()) 
      { 
       // Do Work 
      } 
      using (var c = DatabaseManager.GetOpenConnection()) 
      { 
       // Do more work in same transaction using different connection 
      } 
      ts.Complete(); 
     } 

Pero esto intensifica la transacción:

 using (var ts = new TransactionScope()) 
     { 
      using (var c = DatabaseManager.GetOpenConnection()) 
      { 
       // Do Work 
       using (var nestedConnection = DatabaseManager.GetOpenConnection()) 
       { 
        // Do more work in same transaction using different nested connection - escalated transaction to distributed 
       } 
      } 
      ts.Complete(); 
     } 

¿Existe una práctica recomendada para evitar la escalada de las transacciones de esta manera, mientras que todavía se utilizan conexiones anidados?

Lo mejor que puedo llegar a por el momento es tener una conexión ThreadStatic y la reutilización de que si Transaction.Current se establece, de este modo:

public static class DatabaseManager 
{ 
    private const string _connectionString = "data source=.\\sql2008; initial catalog=test; integrated security=true"; 

    [ThreadStatic] 
    private static SqlConnection _transactionConnection; 

    [ThreadStatic] private static int _connectionNesting; 

    private static SqlConnection GetTransactionConnection() 
    { 
     if (_transactionConnection == null) 
     { 
      Transaction.Current.TransactionCompleted += ((s, e) => 
      { 
       _connectionNesting = 0; 
       if (_transactionConnection != null) 
       { 
        _transactionConnection.Dispose(); 
        _transactionConnection = null; 
       } 
      }); 

      _transactionConnection = new SqlConnection(_connectionString); 
      _transactionConnection.Disposed += ((s, e) => 
      { 
       if (Transaction.Current != null) 
       { 
        _connectionNesting--; 
        if (_connectionNesting > 0) 
        { 
         // Since connection is nested and same as parent, need to keep it open as parent is not expecting it to be closed! 
         _transactionConnection.ConnectionString = _connectionString; 
         _transactionConnection.Open(); 
        } 
        else 
        { 
         // Can forget transaction connection and spin up a new one next time one's asked for inside this transaction 
         _transactionConnection = null; 
        } 
       } 
      }); 
     } 
     return _transactionConnection; 
    } 

    public static SqlConnection GetOpenConnection() 
    { 
     SqlConnection connection; 
     if (Transaction.Current != null) 
     { 
      connection = GetTransactionConnection(); 
      _connectionNesting++; 
     } 
     else 
     { 
      connection = new SqlConnection(_connectionString); 
     } 
     if (connection.State != ConnectionState.Open) 
     { 
      connection.Open(); 
     } 
     return connection; 
    } 
} 

Editar: Por lo tanto, si la respuesta es volver a utilizar el La misma conexión cuando está anidada dentro de un scope de transacción, como lo hace el código anterior, me pregunto acerca de las implicaciones de deshacerse de esta conexión a mitad de la transacción.

Por lo que puedo ver (utilizando Reflector para examinar el código), la configuración de la conexión (cadena de conexión, etc.) se restablece y la conexión se cierra. Entonces (en teoría), volver a establecer la cadena de conexión y abrir la conexión en las llamadas posteriores debería "reutilizar" la conexión y evitar la escalada (y mi prueba inicial coincide con esto).

Aunque parece un poco hacky ... y estoy seguro de que debe haber una mejor práctica en algún lugar que establezca que uno no debe continuar utilizando un objeto después de que haya sido eliminado.

Sin embargo, como no puedo clasificar la SqlConnection sellada, y quiero mantener mis métodos de conexión de agrupación de conexiones independientes de la transacción, me cuesta (pero estaría encantado) ver una mejor manera.

Además, se dio cuenta de que podía forzar las conexiones no anidados por lanzar una excepción si el código de aplicación intenta abrir conexión anidada (que en la mayoría de los casos no es necesario, en nuestra base de código)

public static class DatabaseManager 
{ 
    private const string _connectionString = "data source=.\\sql2008; initial catalog=test; integrated security=true; enlist=true;Application Name='jimmy'"; 

    [ThreadStatic] 
    private static bool _transactionHooked; 
    [ThreadStatic] 
    private static bool _openConnection; 

    public static SqlConnection GetOpenConnection() 
    { 
     var connection = new SqlConnection(_connectionString); 
     if (Transaction.Current != null) 
     { 
      if (_openConnection) 
      { 
       throw new ApplicationException("Nested connections in transaction not allowed"); 
      } 

      _openConnection = true; 
      connection.Disposed += ((s, e) => _openConnection = false); 

      if (!_transactionHooked) 
      { 
       Transaction.Current.TransactionCompleted += ((s, e) => 
       { 
        _openConnection = false; 
        _transactionHooked = false; 
       }); 
       _transactionHooked = true; 
      } 
     } 
     connection.Open(); 
     return connection; 
    } 
} 

seguiría valorar a una menor solución hacky :)

Respuesta

3

Una de las razones principales para la escalada de transacción es cuando tiene múltiples (diferentes) conexiones involucradas en una transacción. Esto casi siempre se convierte en una transacción distribuida. Y de hecho es un dolor.

Es por eso que nos aseguramos de que todas nuestras transacciones utilicen un único objeto de conexión. Hay varias formas de hacer esto. En su mayor parte, utilizamos el objeto estático de subprocesos para almacenar un objeto de conexión, y nuestras clases que funcionan con persistencia de base de datos, utilizan el objeto de conexión estático de subprocesos (que se comparte por supuesto). Esto evita que se utilicen objetos de múltiples conexiones y ha eliminado la escalada de transacciones. También puede lograr esto simplemente pasando un objeto de conexión de método a método, pero esto no es tan limpio, IMO.

+0

:) Agregué mi sugerencia de ThreadStatic mientras escribías esto. ¡Suena como una buena idea! – Kram

+0

@Mark - Nos ha funcionado muy bien y nos evita tener que pasar objetos de conexión. –

+0

@Randy - Gracias - ¿Tiene una mejor manera de detectar cuándo se ha invocado el uso de dispose() contra la conexión de hilos (como puede ver en mi ejemplo anterior, simplemente reajustaré la cadena de conexión y todo _seems_ está bien? Aunque me parece un poco hacky – Kram

Cuestiones relacionadas