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

usr*_*usr 53 t-sql sql-server ado.net transactions transactionscope

正如先前的Stack Overflow问题(TransactionScope和连接池以及SqlConnection如何管理IsolationLevel?)所证明的那样,事务隔离级别在SQL Server和ADO.NET(也是System.Transactions和EF)的池化连接中泄漏,因为它们建立在ADO.NET).

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

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

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

这是一个独立的复制品.您将看到第三个查询将继承第二个查询中的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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

输出:

Isolation Level = ReadCommitted, SPID = 51
Isolation Level = Serializable, SPID = 51
Isolation Level = Serializable, SPID = 51 //leaked!
Run Code Online (Sandbox Code Playgroud)

And*_*mar 30

连接池在回收连接之前调用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
Run Code Online (Sandbox Code Playgroud)

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

最后,您可以确保每个"可序列化"查询在返回之前重置隔离级别.如果"serializable"查询无法完成,您可以清除连接池以强制将污染连接从池中取出:

SqlConnection.ClearPool(yourSqlConnection);
Run Code Online (Sandbox Code Playgroud)

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

  • 此行为是'按设计':http://connect.microsoft.com/SQLServer/feedback/details/243527/sp-reset-connection-doesnt-reset-isolation-level (5认同)
  • 注意:在连接字符串的末尾添加空格足以使其来自不同的池。这真的是我正在考虑的方法:-/ (3认同)
  • 我们去了连接字符串路由.如果Transaction.Current不为null,我们更改"应用程序名称" (2认同)
  • 对不同的隔离级别使用不同的连接字符串对我来说很有意义 (2认同)

Tho*_*mas 19

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

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

ReadCommitted
Serializable
ReadCommitted
Run Code Online (Sandbox Code Playgroud)

遗憾的是,此更改未在任何文档中提及,例如:

但这一变化已记录在微软论坛上.

更新2017-03-08

不幸的是,这在SQL Server 2014 CU6和SQL Server 2014 SP1 CU1中后来"不固定",因为它引入了一个错误:

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

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

  • 使用Sql2014庆祝可能为时过早 - 请参阅此处:http://support.microsoft.com/en-us/kb/3025845 (3认同)
  • 此问题仍在 SQL Server 2016 SP1 CU5 上发生,在 Windows Server 2016 上运行 .Net 4.6 客户端 (2认同)

Mic*_*elD 5

对于在 .NET 中使用 EF 的用户,您可以通过为每个隔离级别设置不同的应用程序名称来修复整个应用程序的此问题(也如 @Andomar 所述):

//prevent isolationlevel leaks
///sf/ask/689599081/
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);
}
Run Code Online (Sandbox Code Playgroud)

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

  • 非常有创意的解决方案! (2认同)