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