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

时间:2012-03-24 11:38:19

标签: sql-server tsql ado.net transactions transactionscope

正如之前的Stack Overflow问题(TransactionScope and Connection PoolingHow does SqlConnection manage IsolationLevel?)所证明的那样,事务隔离级别在与SQL Server和ADO.NET(也是System.Transactions和EF,因为它们构建)的池化连接中泄漏在ADO.NET之上。)

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

  1. 发生请求,需要显式事务以确保数据一致性
  2. 任何其他请求都不会使用显式事务,因为它只进行非关键的读取。此请求现在将作为可序列化执行,可能导致危险的阻塞和死锁
  3. 问题:防止这种情况的最佳方法是什么?现在到处都需要使用显式交易?

    这是一个独立的复制品。您将看到第三个查询将继承第二个查询中的Serializable级别。

    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!
    

4 个答案:

答案 0 :(得分:28)

连接池在回收连接之前调用sp_resetconnection。重置事务隔离级别是sp_resetconnection所做的not in the list of things。这可以解释为什么“可序列化”泄漏到池化连接。

我猜您可以通过确保它位于right isolation level

来启动每个查询
if not exists (
              select  * 
              from    sys.dm_exec_sessions 
              where   session_id = @@SPID 
                      and transaction_isolation_level = 2
              )
    set transaction isolation level read committed

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

最后,您可以确保每个“可序列化”查询在返回之前重置隔离级别。如果“序列化”查询无法完成,您可以clear the connection pool强制将受污染的连接从池中取出:

SqlConnection.ClearPool(yourSqlConnection);

这可能很昂贵,但查询失败很少,因此您不必经常致电ClearPool()

答案 1 :(得分:18)

SQL Server 2014 中,这似乎已得到修复。如果使用TDS protocol 7.3或更高版本。

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

ReadCommitted
Serializable
ReadCommitted

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

But the change has been documented on a Microsoft Forum.

更新2017-03-08

不幸的是,这是后来的"未经修复的"在SQL Server 2014 CU6和SQL Server 2014 SP1 CU1中引入了一个错误:

FIX: The transaction isolation level is reset incorrectly when the SQL Server connection is released in SQL Server 2014

  

"假设您在SQL Server客户端源代码中使用TransactionScope类,并且未在事务中显式打开SQL Server连接。释放SQL Server连接时,将错误地重置事务隔离级别。"

答案 2 :(得分:1)

对于在.NET中使用EF的用户,可以通过为每个隔离级别设置不同的应用程序名称(也由@Andomar声明)来为整个应用程序解决此问题:

//prevent isolationlevel leaks
//https://stackoverflow.com/questions/9851415/sql-server-isolation-level-leaks-across-pooled-connections
public static DataContext CreateContext()
{
    string isolationlevel = Transaction.Current?.IsolationLevel.ToString();
    string connectionString = ConfigurationManager.ConnectionStrings["yourconnection"].ConnectionString;
    connectionString = Regex.Replace(connectionString, "APP=([^;]+)", "App=$1-" + isolationlevel, RegexOptions.IgnoreCase);

    return new DataContext(connectionString);
}

奇怪的是,八年后这仍然是一个问题...

答案 3 :(得分:0)

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

Change isolation level in individual ADO.NET transactions only

它基本上是一个包含在'使用' block,它在之前查询原始隔离级别并在以后恢复它。

但是,它确实需要两次额外的往返DB来检查和恢复默认的隔离级别,我并不完全确定它永远不会泄漏更改的隔离级别,尽管我看到的危险很小。