web-dev-qa-db-fra.com

SQL Server: fuite du niveau d'isolement sur les connexions en pool

Comme démontré dans les questions précédentes sur le dépassement de capacité ( TransactionScope et mise en pool de connexions et Comment SqlConnection gère-t-il IsolationLevel? ), le niveau d'isolation de transaction fuit entre les connexions en pool avec SQL Server et ADO.NET (également System.Transactions et EF, car ils reposent sur ADO.NET).

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

  1. Une demande survient qui nécessite une transaction explicite pour assurer la cohérence des données
  2. Toute autre requête n'utilise pas de transaction explicite, car elle ne fait que des lectures non critiques. Cette demande sera maintenant exécutée en tant que serializable, provoquant potentiellement un blocage dangereux et des blocages

La question: Quel est le meilleur moyen d’empêcher 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 Serializable 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!
48
usr

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

Sous SQL Server version 12.0.2000.8, la sortie est la suivante:

ReadCommitted
Serializable
ReadCommitted

Malheureusement, cette modification n’est mentionnée dans aucune documentation telle que:

Mais le changement a été documenté sur un forum Microsoft.

Mise à jour 2017-03-08

Malheureusement, cela a été "non corrigé" par la suite dans SQL Server 2014 CU6 et SQL Server 2014 SP1 CU1 car il introduisait un bogue:

CORRECTIF: le niveau d'isolation de transaction est réinitialisé de manière incorrecte lorsque la connexion SQL Server est libérée dans SQL Server 2014

"Supposons que vous utilisiez la classe TransactionScope dans le code source côté client SQL Server et que vous n'ouvriez 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é de manière incorrecte."

18
Thomas

Le pool de connexions appelle sp_resetconnection avant de recycler une connexion. La réinitialisation du niveau d'isolation de transaction est pas dans la liste des choses que sp_resetconnection fait. Cela expliquerait pourquoi les "sérialisables" fuient sur des connexions en pool.

Je suppose que vous pouvez commencer chaque requête en vous assurant qu’il s’agit du bon niveau d’isolation :

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: les connexions avec une chaîne de connexion différente ne partagent pas un pool de connexion. Par conséquent, si vous utilisez une autre chaîne de connexion pour les requêtes "sérialisables", elles ne partageront pas un pool avec les requêtes "lecture validée". Un moyen simple de modifier la chaîne de connexion consiste à utiliser un identifiant différent. Vous pouvez également ajouter une option aléatoire telle que Persist Security Info=False;.

Enfin, vous pouvez vous assurer que chaque requête "sérialisable" réinitialise le niveau d'isolation avant son retour. Si une requête "sérialisable" échoue, vous pouvez effacer le pool de connexions pour forcer la connexion corrompue à sortir du pool:

SqlConnection.ClearPool(yourSqlConnection);

Ceci est potentiellement coûteux, mais les requêtes qui échouent sont rares, vous ne devriez donc pas avoir à appeler ClearPool() souvent.

27
Andomar

Je viens de poser une question sur ce sujet et d’ajouter un morceau de code C #, qui peut aider à résoudre ce problème (ce qui signifie: changer le niveau d’isolement pour une seule transaction).

Modifier le niveau d'isolation dans les transactions ADO.NET individuelles uniquement

Il s’agit d’une classe à encapsuler dans un bloc "using", qui interroge le niveau d’isolement initial avant et le restaure ultérieurement.

Cependant, il faut deux allers-retours supplémentaires à la base de données pour vérifier et restaurer le niveau d'isolation par défaut, et je ne suis pas absolument sûr que le niveau d'isolation modifié ne fuira jamais, bien que je ne voie que très peu de danger.

0
Erik Hart