SQL Server:跨池连接的隔离级别泄漏

正如前面的Stack Overflow问题( TransactionScope和Connection Pooling以及SqlConnection如何pipe理IsolationLevel? )所展示的,事务隔离级别在与SQL Server和ADO.NET(也是System.Transactions和EF)的池化连接之间泄漏,因为它们构build在ADO.NET)。

这意味着,任何应用程序都可能发生以下危险的事件序列:

  1. 发生请求需要显式事务来确保数据一致性
  2. 任何其他请求都不使用显式事务,因为它只是在进行不重要的读取。 这个请求现在将作为可序列化执行, 可能导致危险的阻塞和死锁

问题: 防止这种情况的最好方法是什么? 现在到处都需要使用明确的交易吗?

这里是一个独立的repro。 您将看到第三个查询将从第二个查询inheritanceSerializable级别。

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

输出:

 Isolation Level = ReadCommitted, SPID = 51 Isolation Level = Serializable, SPID = 51 Isolation Level = Serializable, SPID = 51 //leaked! 

SQL Server 2014中,这似乎已经修复。 如果使用TDS协议7.3或更高版本。

在SQL Server版本12.0.2000.8上运行,输出是:

 ReadCommitted Serializable ReadCommitted 

不幸的是,这种改变在任何文档中都没有提到,例如:

  • SQL Server 2014中数据库引擎function的行为更改
  • 在SQL Server 2014中突破对数据库引擎function的改变

但是这个改变已经在微软论坛上有logging。

更新2017-03-08

不幸的是,后来在SQL Server 2014 CU6和SQL Server 2014 SP1 CU1中“不固定”,因为它引入了一个bug:

FIX:在SQL Server 2014中释放SQL Server连接时,事务隔离级别被错误地重置

“假设您在SQL Server客户端源代码中使用了TransactionScope类,并且您没有明确地打开事务中的SQL Server连接。当SQL Server连接被释放时,事务隔离级别被错误地重置。

连接池在回收连接之前调用sp_resetconnection。 重置事务隔离级别不在 sp_resetconnection所做的事情列表中。 这就可以解释为什么“可序列化”泄漏在连接池中。

我想你可以通过确保它在正确的隔离级别来启动每个查询:

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

另一个选项:与不同连接string的连接不共享连接池。 因此,如果对“可序列化”查询使用另一个连接string,则它们不会与“读取已提交”查询共享一个池。 更改连接string的简单方法是使用其他login名。 你也可以添加一个随机选项,如Persist Security Info=False;

最后,可以确保每个“可序列化”查询在返回之前重置隔离级别。 如果“可序列化”查询无法完成,则可以清除连接池以强制将受污染的连接移出池:

 SqlConnection.ClearPool(yourSqlConnection); 

这可能是很昂贵的,但是失败的查询很less见,所以你不应该经常调用ClearPool()

我只是问了一个关于这个主题的问题,并添加了一段C#代码,它可以帮助解决这个问题(意思是:只改变一个事务的隔离级别)。

仅在单个ADO.NET事务中更改隔离级别

它基本上是一个包装在“使用”块中的类,它在之前查询原始隔离级别,稍后恢复。

但是,它确实需要额外的两次往返数据库来检查和恢复默认的隔离级别,而且我不能绝对确定它绝不会泄漏改变的隔离级别,尽pipe我几乎看不到这种危险。