SQL Server:跨池连接的隔离级别泄漏
Posted
技术标签:
【中文标题】SQL Server:跨池连接的隔离级别泄漏【英文标题】:SQL Server: Isolation level leaks across pooled connections 【发布时间】:2021-12-23 23:00:31 【问题描述】:正如之前的 Stack Overflow 问题(TransactionScope and Connection Pooling 和 How does SqlConnection manage IsolationLevel?)所证明的,事务隔离级别在与 SQL Server 和 ADO.NET(也包括 System.Transactions 和 EF)的池连接之间泄漏,因为它们构建在 ADO 之上.NET)。
这意味着,在任何应用程序中都可能发生以下危险的事件序列:
-
发生需要显式事务以确保数据一致性的请求
任何其他不使用显式事务的请求进入,因为它只执行非关键读取。此请求现在将作为可序列化执行,可能会导致危险的阻塞和死锁
问题:防止这种情况的最佳方法是什么?现在真的需要在各处使用显式事务吗?
这是一个独立的复制品。您将看到第三个查询将继承第二个查询的 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!
【问题讨论】:
【参考方案1】:连接池在回收连接之前调用 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()
。
【讨论】:
此行为是“设计使然”:connect.microsoft.com/SQLServer/feedback/details/243527/… 接受这个,因为它表明这种行为是设计使然。似乎没有一个好的解决方案。 我们走的是连接字符串路线。如果 Transaction.Current 不为空,我们更改“应用程序名称” 为不同的隔离级别使用不同的连接字符串对我来说很有意义 注意:在连接字符串的末尾添加空格足以让它来自不同的池。这是我认真考虑的方法:-/【参考方案2】:在 SQL Server 2014 中,这似乎已得到修复。如果使用TDS protocol 7.3 或更高版本。
在 SQL Server 版本 12.0.2000.8 上运行,输出为:
ReadCommitted
Serializable
ReadCommitted
不幸的是,任何文档中都没有提及此更改,例如:
Behavior Changes to Database Engine Features in SQL Server 2014 Breaking Changes to Database Engine Features in SQL Server 2014But 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连接时,事务隔离级别被错误重置。 "
解决方法
看来,由于传递参数会使驱动程序使用sp_executesql
,这会强制使用新范围,类似于存储过程。批处理结束后范围回滚。
因此,为避免泄漏,请传递一个虚拟参数,如下所示。
using (var conn = new SqlConnection(connString))
using (var comm = new SqlCommand(@"
SELECT transaction_isolation_level FROM sys.dm_exec_sessions where session_id = @@SPID
", conn))
conn.Open();
Console.WriteLine(comm.ExecuteScalar());
using (var conn = new SqlConnection(connString))
using (var comm = new SqlCommand(@"
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
SELECT transaction_isolation_level FROM sys.dm_exec_sessions where session_id = @@SPID
", conn))
comm.Parameters.Add("@dummy", SqlDbType.Int).Value = 0; // see with and without
conn.Open();
Console.WriteLine(comm.ExecuteScalar());
using (var conn = new SqlConnection(connString))
using (var comm = new SqlCommand(@"
SELECT transaction_isolation_level FROM sys.dm_exec_sessions where session_id = @@SPID
", conn))
conn.Open();
Console.WriteLine(comm.ExecuteScalar());
【讨论】:
看起来不错。我正在等待官方确认。如果您发现任何内容,请在此处发表评论。连接问题还没有。 无论如何,SQL 2005、2008 和 2012 仍然会在一段时间内用于大多数业务应用程序,但很高兴看到事务最终成为事务性,就隔离级别而言。 用 Sql2014 庆祝可能为时过早 - 请参阅此处:support.microsoft.com/en-us/kb/3025845 我刚刚在 SQL Server 2014 Standard SP4 CU2 上测试了这个,第三个连接是可序列化的,即修复似乎不存在。 在 Windows Server 2016 上运行 .Net 4.6 客户端的 SQL Server 2016 SP1 CU5 上仍会出现此问题【参考方案3】:对于那些在 .NET 中使用 EF 的用户,您可以通过为每个隔离级别设置不同的应用程序名称来解决整个应用程序的问题(@Andomar 也指出):
//prevent isolationlevel leaks
//https://***.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);
奇怪的是,8年后这仍然是一个问题......
【讨论】:
非常有创意的解决方案! 这个工作的原因是连接池是每个连接字符串的,所以你可以修改连接字符串中的任何内容来获得一个新的池。【参考方案4】:我刚刚问了一个关于这个话题的问题,并添加了一段 C# 代码,可以帮助解决这个问题(意思是:只为一个事务更改隔离级别)。
Change isolation level in individual ADO.NET transactions only
它基本上是一个被包装在一个'using'块中的类,它查询之前的原始隔离级别并稍后恢复它。
然而,它确实需要两次额外的数据库往返来检查和恢复默认隔离级别,我不确定它永远不会泄漏更改后的隔离级别,尽管我认为这种风险很小。
【讨论】:
以上是关于SQL Server:跨池连接的隔离级别泄漏的主要内容,如果未能解决你的问题,请参考以下文章