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

Sql server SQL Server:跨池连接的隔离级别泄漏,sql-server,tsql,ado.net,transactions,transactionscope,Sql Server,Tsql,Ado.net,Transactions,Transactionscope,如前面的堆栈溢出问题(和)所示,事务隔离级别在SQL Server和ADO.NET的池连接(以及System.Transactions和EF,因为它们构建在ADO.NET之上)之间泄漏 这意味着,在任何应用中都可能发生以下危险事件序列: 发生需要显式事务以确保数据一致性的请求 任何其他请求都不会使用显式事务,因为它只进行非关键读取。此请求现在将以可序列化的方式执行,可能导致危险的阻塞和死锁 问题是:防止这种情况的最佳方法是什么?现在真的需要在任何地方使用显式事务吗 这是一份独立的复印件。您将看到

如前面的堆栈溢出问题(和)所示,事务隔离级别在SQL Server和ADO.NET的池连接(以及System.Transactions和EF,因为它们构建在ADO.NET之上)之间泄漏

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

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

    这是一份独立的复印件。您将看到,第三个查询将从第二个查询继承可序列化级别

    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!
    

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

    我想您可以通过确保在以下位置开始每个查询:

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

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

    SqlConnection.ClearPool(yourSqlConnection);
    

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

    SQL Server 2014中,这似乎已经得到了修复。如果使用或更高

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

    ReadCommitted
    Serializable
    ReadCommitted
    
    不幸的是,任何文档中均未提及此更改,例如:

    更新2017-03-08 不幸的是,这后来在SQL Server 2014 CU6和SQL Server 2014 SP1 CU1中被“未修复”,因为它引入了一个bug:

    假定您在SQL Server客户端源代码中使用TransactionScope类,并且没有在事务中显式打开SQL Server连接。释放SQL Server连接时,事务隔离级别重置不正确

    变通办法 看起来,由于传递参数会使驱动程序使用
    sp_executesql
    ,这会强制使用一个新的作用域,类似于存储过程。该范围在批处理结束后回滚

    因此,为了避免泄漏,请传递一个伪参数,如下所示

    使用(var conn=new SqlConnection(connString))
    使用(var comm=new SqlCommand)(@)
    从sys.dm\u exec\u会话中选择事务\u隔离\u级别,其中会话\u id=@@SPID
    (康涅狄格州)
    {
    conn.Open();
    Console.WriteLine(comm.ExecuteScalar());
    }
    使用(var conn=new SqlConnection(connString))
    使用(var comm=new SqlCommand)(@)
    设置事务隔离级别快照;
    从sys.dm\u exec\u会话中选择事务\u隔离\u级别,其中会话\u id=@@SPID
    (康涅狄格州)
    {
    comm.Parameters.Add(“@dummy”,SqlDbType.Int).Value=0;//查看有无
    conn.Open();
    Console.WriteLine(comm.ExecuteScalar());
    }
    使用(var conn=new SqlConnection(connString))
    使用(var comm=new SqlCommand)(@)
    从sys.dm\u exec\u会话中选择事务\u隔离\u级别,其中会话\u id=@@SPID
    (康涅狄格州)
    {
    conn.Open();
    Console.WriteLine(comm.ExecuteScalar());
    }
    
    我刚刚就这个主题提出了一个问题,并添加了一段C代码,它可以帮助解决这个问题(意思是:只为一个事务更改隔离级别)

    它基本上是一个要包装在“using”块中的类,该块在之前查询原始隔离级别,然后将其恢复


    但是,它确实需要两次额外的DB往返来检查和恢复默认隔离级别,我不能绝对确定它是否永远不会泄漏更改后的隔离级别,尽管我认为这样做的危险很小。

    对于那些在.NET中使用EF的人,您可以通过为每个隔离级别设置不同的appname(如@Andomar所述),为整个应用程序解决此问题:


    奇怪的是,8年后这仍然是一个问题……

    这种行为是“设计的”:接受它,因为它表明这种行为是设计的。似乎没有一个好的解决方案。我们使用了连接字符串路径。如果Transaction.Current不为null,我们将使用不同的连接字符串为不同的隔离级别更改“应用程序名称”,这对meNote很有意义:在连接字符串的末尾添加空格足以使其来自不同的池。这是我认真考虑的方法:-/看起来很棒。我在等官方确认。如果您注意到任何问题,请在此处留下评论。连接问题还没有解决。无论如何,在一段时间内,大多数业务应用程序仍将有SQL 2005、2008和2012,但很高兴看到事务最终成为事务性的,就隔离级别而言,使用Sql2014可能还为时过早-请参见此处:我刚刚在SQL Server 2014标准SP4 CU2上测试了这一点,第三个连接是可序列化的,ie修复程序似乎不存在。SQL Server 2016 SP1 CU5上仍存在此问题,Windows Server 2016上运行的.Net 4.6客户端非常有创意的解决方案!之所以这样做,是因为连接池是针对每个连接字符串的,所以您可以修改连接池中的任何内容
    ReadCommitted
    Serializable
    ReadCommitted
    
    //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);
    }