10

L'utilisation de l'objet TransactionScope pour configurer une transaction implicite qui n'a pas besoin d'être transmise entre les appels de fonction est excellente! Cependant, si une connexion est ouverte alors qu'une autre est déjà ouverte, le coordinateur de transaction escalade silencieusement la transaction à distribuer (nécessitant que le service MSDTC fonctionne et prenne beaucoup plus de ressources et de temps).Pratique recommandée pour l'arrêt des transactions escaladées à distribuées lors de l'utilisation de transactionscope

Donc, cela est 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(); 
     } 

Mais la transaction les aggraver:

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

Est-il recommandé d'éviter l'escalade des transactions de cette façon, tout en utilisant encore des connexions imbriquées?

Le mieux que je peux venir avec au moment est d'avoir une connexion ThreadStatic et la réutilisation que si Transaction.Current est défini, comme ceci:

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

Edit: Donc, si la réponse est de réutiliser le même connexion quand il est imbriqué dans un transactionscope, comme le fait le code ci-dessus, je m'interroge sur les implications de disposer de cette connexion à mi-transaction.

Autant que je puisse voir (en utilisant Reflector pour examiner le code), les paramètres de la connexion (chaîne de connexion, etc.) sont réinitialisés et la connexion est fermée. Donc (en théorie), la réinitialisation de la chaîne de connexion et l'ouverture de la connexion sur les appels suivants devraient "réutiliser" la connexion et empêcher l'escalade (et mon test initial est en accord avec cela). Cela semble un peu hacky cependant ... et je suis sûr qu'il doit y avoir une meilleure pratique quelque part qui stipule qu'on ne devrait pas continuer à utiliser un objet après qu'il a été éliminé! Cependant, étant donné que je ne peux pas sous-classer la SqlConnection scellée, et que je veux maintenir mes méthodes conviviales de pool de connexion agnostique, je lutte (mais serais ravi) de voir une meilleure façon de procéder.

En outre, rendu compte que je pouvais forcer les connexions non imbriquées en jetant exception si le code d'application tente d'ouvrir une connexion imbriquée (qui, dans la plupart des cas est inutile, dans notre base de code)

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

apprécieraient encore moins solution hacky :)

Répondre

3

L'une des raisons principales de l'escalade de transaction est lorsque vous avez plusieurs connexions (différentes) impliquées dans une transaction. Cela devient presque toujours une transaction distribuée. Et c'est en effet une douleur.

C'est pourquoi nous nous assurons que toutes nos transactions utilisent un seul objet de connexion. Il y a plusieurs moyens de le faire. Pour l'essentiel, nous utilisons l'objet statique de thread pour stocker un objet de connexion, et nos classes qui font la persistance de base de données fonctionnent, utilisent l'objet de connexion statique de thread (qui est bien sûr partagé). Cela empêche l'utilisation de plusieurs objets de connexions et a éliminé l'escalade des transactions. Vous pouvez également réaliser cela en passant simplement un objet de connexion de la méthode à la méthode, mais ce n'est pas aussi propre, IMO.

+0

:) J'ai ajouté ma suggestion ThreadStatic pendant que vous écrivez ceci. Ça semble être une bonne idée! – Kram

+0

@Mark - Cela a très bien fonctionné pour nous, et cela nous évite d'avoir à passer des objets de connexion. –

+0

@Randy - Merci - avez-vous une meilleure façon de détecter quand dispose() a été appelé contre la connexion threadstatic (comme vous pouvez le voir dans mon exemple ci-dessus, je réinitialise simplement la chaîne de connexion et tout est correct. Semble un peu hacky à moi si – Kram

Questions connexes