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:
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!
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.
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:
"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."
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.
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.