SQL Server: fuites du niveau D'Isolation entre les connexions groupées

comme le démontrent les questions précédentes sur le débordement de la pile ( TransactionScope and Connection Pooling et comment SqlConnection gère-t-il L'IsolationLevel? ), le niveau d'isolation de la transaction fuit à travers les connexions groupées avec SQL Server et ADO.NET (également Système.Les Transactions et le FE, parce qu'ils s'appuient sur ADO.NET).

cela signifie que la séquence dangereuse suivante d'événements peut se produire dans n'importe quelle application:

  1. une demande qui nécessite une transaction explicite pour assurer la cohérence des données
  2. toute autre requête vient dans laquelle n'utilise pas une transaction explicite parce qu'il ne fait que des lectures non critiques. Cette requête va maintenant s'exécuter comme serialisable, causant potentiellement des blocages dangereux et des blocages

à La question: Quelle est la meilleure façon de prévenir ce scénario? Est-il vraiment nécessaire d'utiliser des transactions explicites partout maintenant?

voici un repro autonome. Vous verrez que la troisième requête aura hérité du niveau Serialisable de la deuxième requête.

class Program
{
    static void Main(string[] args)
    {
        RunTest(null);
        RunTest(IsolationLevel.Serializable);
        RunTest(null);
        Console.ReadKey();
    }

    static void RunTest(IsolationLevel? isolationLevel)
    {
        using (var tran = isolationLevel == null ? null : new TransactionScope(0, new TransactionOptions() { IsolationLevel = isolationLevel.Value }))
        using (var conn = new SqlConnection("Data Source=(local); Integrated Security=true; Initial Catalog=master;"))
        {
            conn.Open();

            var cmd = new SqlCommand(@"
select         
        case transaction_isolation_level 
            WHEN 0 THEN 'Unspecified' 
            WHEN 1 THEN 'ReadUncommitted' 
            WHEN 2 THEN 'ReadCommitted' 
            WHEN 3 THEN 'RepeatableRead' 
            WHEN 4 THEN 'Serializable' 
            WHEN 5 THEN 'Snapshot' 
        end as lvl, @@SPID
     from sys.dm_exec_sessions 
    where session_id = @@SPID", conn);

            using (var reader = cmd.ExecuteReader())
            {
                while (reader.Read())
                {
                    Console.WriteLine("Isolation Level = " + reader.GetValue(0) + ", SPID = " + reader.GetValue(1));
                }
            }

            if (tran != null) tran.Complete();
        }
    }
}

sortie:

Isolation Level = ReadCommitted, SPID = 51
Isolation Level = Serializable, SPID = 51
Isolation Level = Serializable, SPID = 51 //leaked!
46
demandé sur Community 2012-03-24 15:38:19

3 réponses

Dans SQL Server 2014 cela semble avoir été corrigé. Si vous utilisez protocole TDS 7.3 ou plus.

tournant sur SQL Server version 12.0.2000.8 la sortie est:

ReadCommitted
Serializable
ReadCommitted

malheureusement ce changement n'est mentionné dans aucune documentation telle que:

mais le changement a été documenté sur un forum de Microsoft.

mise à jour 2017-03-08

malheureusement cela a été plus tard "unfixed" dans SQL Server 2014 CU6 et SQL Server 2014 SP1 CU1 depuis qu'il a introduit un bug:

FIX: Le niveau d'isolation de transaction est réinitialisé incorrectement lorsque la connexion au serveur SQL est libérée dans SQL Server 2014

" supposons que vous utilisez la classe Transactionsscope dans le code source SQL Server côté client, et que vous n'ouvrez pas explicitement la connexion SQL Server dans une transaction. Lorsque la connexion SQL Server est libérée, le niveau d'isolation de la transaction est réinitialisé incorrectement."

18
répondu Thomas 2017-03-08 12:40:53

le pool de connexion appelle sp_resetconnection avant de recycler une connexion. Réinitialiser le niveau d'isolation de transaction est pas dans la liste des choses que sp_resetconnection. Cela expliquerait pourquoi les fuites" sérialisables " à travers les connexions groupées.

je suppose que vous pourriez commencer chaque requête en s'assurant qu'elle est au niveau d'isolation droit :

if not exists (
              select  * 
              from    sys.dm_exec_sessions 
              where   session_id = @@SPID 
                      and transaction_isolation_level = 2
              )
    set transaction isolation level read committed

une autre option: connexions avec une chaîne de connexion différente, ne partagez pas de pool de connexion. Ainsi, si vous utilisez une autre chaîne de connexion pour les requêtes "serialisable", ils ne partageront pas de pool avec les requêtes "READ committed". Un moyen facile de modifier la chaîne de connexion est d'utiliser une autre connexion. Vous pouvez également ajouter une option aléatoire comme Persist Security Info=False; .

enfin, vous pouvez vous assurer que chaque requête "sérialisable" réinitialise le niveau d'isolation avant de revenir. Si une requête "sérialisable" échoue complet, vous pouvez effacer la piscine de connexion pour forcer la connexion contaminée hors de la piscine:

SqlConnection.ClearPool(yourSqlConnection);

c'est potentiellement cher, mais les requêtes qui échouent sont rares, donc vous ne devriez pas avoir à appeler ClearPool() souvent.

25
répondu Andomar 2017-05-23 12:34:53

je viens de poser une question sur ce sujet et j'ai ajouté un morceau de code C, qui peut aider autour de ce problème (ce qui signifie: changer le niveau d'isolement seulement pour une transaction).

Changement de niveau d'isolement dans les différents ADO.NET seules transactions

il s'agit essentiellement d'une classe à envelopper dans un bloc "using", qui interroge le niveau d'isolation d'origine avant et le restitue plus tard.

il fait, cependant, nécessite deux allers-retours supplémentaires vers le DB pour vérifier et restaurer le niveau d'isolation par défaut, et je ne suis pas absolument sûr qu'il ne fuira jamais le niveau d'isolation modifié, bien que je vois très peu de danger de cela.

0
répondu Erik Hart 2017-05-23 12:10:47