带有 TransactionScope 的 EF6 - IsolationLevel.ReadUncommitted 但首先获得 ReadCommitted
EF6 with TransactionScope - IsolationLevel.ReadUncommitted but got ReadCommitted first
在 MSSQL 2008
上使用 EF 进行查询更新时存在性能和锁定问题。所以我设置了ReadUncommitted事务隔离级别,希望能解决,像这样,
之前
using (MyEntities db = new MyEntities())
{
// large dataset
var data = from _Contact in db.Contact where _Contact.MemberId == 13 select _Contact;
for (var item in data)
item.Flag = 0;
// Probably db lock
db.SaveChanges();
}
之后
using (var scope =
new TransactionScope(TransactionScopeOption.RequiresNew,
new TransactionOptions() { IsolationLevel = IsolationLevel.ReadUncommitted }))
{
using (MyEntities db = new MyEntities())
{
// large dataset but with NOLOCK
var data = from _Contact in db.Contact where _Contact.MemberId == 13 select _Contact;
for (var item in data)
item.Flag = 0;
// Try avoid db lock
db.SaveChanges();
}
}
我们用SQL profiler
来追踪。但是,按顺序获取这些脚本,
(预计第一个脚本未提交读取。)
审核登录
set transaction isolation level read committed
SP:StmtStarting
SELECT
[Extent1].[ContactId] AS [ContactId],
[Extent1].[MemberId] AS [MemberId],
FROM [dbo].[Contact] AS [Extent1]
WHERE [Extent1].[MemberId] = @p__linq__0
审核登录
set transaction isolation level read uncommitted
虽然我可以重新发送这个请求并使其正确排序(将对以下请求显示 read-uncommitted
,相同的 SPID),但我想知道为什么它发送了 read-读取提交命令后未提交的命令以及如何使用 EF 和 TransactionScope 修复?谢谢。
我认为更好的解决方案是通过生成直接查询来执行更新(而不是 selection 并逐个更新实体)。为了使用对象而不是查询,您可以使用 EntityFramework.Extended:
db.Contact.Update(C => c.MemberId == 13, c => new Contact { Flag = 0 });
这应该会生成类似于 UPDATE Contact SET Flag = 0 WHERE MemberId = 13
的内容,它比您当前的解决方案要快得多。
如果我没记错的话,这应该会生成自己的交易。如果这必须在与其他查询的事务中执行,仍然可以使用 `TransactionScope(您将有两个事务)。
此外,隔离级别可以保持不变(ReadCommitted)。
[编辑]
Chris'
的分析准确地显示了发生的事情。为了使其更具相关性,以下代码显示了 TransactionScope
:
内部和外部的差异
using (var db = new myEntities())
{
// this shows ReadCommitted
Console.WriteLine($"Isolation level outside TransactionScope = {db.Database.SqlQuery(typeof(string), selectIsolationLevel).Cast<string>().First()}");
}
using (var scope =
new TransactionScope(TransactionScopeOption.RequiresNew,
new TransactionOptions() { IsolationLevel = IsolationLevel.ReadUncommitted }))
{
// this show ReadUncommitted
Console.WriteLine($"Isolation level inside TransactionScope = {db.Database.SqlQuery(typeof(string), selectIsolationLevel).Cast<string>().First()}");
using (myEntities db = new myEntities ())
{
var data = from _Contact in db.Contact where _Contact.MemberId == 13 select _Contact; // large results but with nolock
for (var item I data)
item.Flag = 0;
db.SaveChanges(); // Try avoid db lock
}
// this should be added to actually Commit the transaction. Otherwise it will be rolled back
scope.Complete();
}
回到实际问题(陷入僵局),如果我们看一下 Profiler 在整个过程中输出的内容,我们会看到类似这样的内容(已删除 GO
s):
BEGIN TRANSACTION
SELECT <all columns> FROM Contact
exec sp_reset_connection
exec sp_executesql N'UPDATE Contact
SET [Flag] = @0
WHERE ([Contact] = @1)
',N'@0 nvarchar(1000),@1 int',@0=N'1',@1=1
-- lots and lots of other UPDATEs like above
-- or ROLLBACK if scope.Complete(); is missed
COMMIT
这有两个缺点:
许多 round-trips - 对数据库发出了很多查询,这给数据库引擎带来了更大的压力,并且客户端也需要更长的时间
长交易 - 应避免长交易作为 minimizing deadlocks
的暂定
因此,建议的解决方案应该更适合您的特定情况(简单更新)。
在更复杂的情况下,可能需要更改隔离级别。
我认为,如果要处理大量数据(select 数百万、做某事、更新回来等),存储过程可能是解决方案,因为所有内容都会执行 server-side。
我认为这是依赖审计登录事件引起的转移注意力。这是 而不是 显示客户端告诉服务器 'set transaction isolation level read uncommitted' 的时刻。当从连接池中取出并重新使用该连接时,它向您展示稍后的隔离级别。
我通过将 Pooling=false
添加到我的连接字符串来验证这一点。然后,审计登录总是显示事务隔离级别读取已提交。
到目前为止,我在 SQL Profiler 中找不到任何方法来查看 EF 设置事务级别的时刻,也没有任何显式 begin tran
.
我可以通过阅读和记录级别来确认它正在某处设置:
const string selectIsolationLevel = @"SELECT CASE transaction_isolation_level WHEN 0 THEN 'Unspecified' WHEN 1 THEN 'ReadUncommitted' WHEN 2 THEN 'ReadCommitted' WHEN 3 THEN 'Repeatable' WHEN 4 THEN 'Serializable' WHEN 5 THEN 'Snapshot' END AS TRANSACTION_ISOLATION_LEVEL FROM sys.dm_exec_sessions where session_id = @@SPID";
static void ReadUncommitted()
{
using (var scope =
new TransactionScope(TransactionScopeOption.RequiresNew,
new TransactionOptions{ IsolationLevel = IsolationLevel.ReadUncommitted }))
using (myEntities db = new myEntities())
{
Console.WriteLine("Read is about to be performed with isolation level {0}",
db.Database.SqlQuery(typeof(string), selectIsolationLevel).Cast<string>().First()
);
var data = from _Contact in db.Contact where _Contact.MemberId == 13 select _Contact; // large results but with nolock
foreach (var item in data)
item.Flag = 0;
//Using Nuget package https://www.nuget.org/packages/Serilog.Sinks.Literate
//logger = new Serilog.LoggerConfiguration().WriteTo.LiterateConsole().CreateLogger();
//logger.Information("{@scope}", scope);
//logger.Information("{@scopeCurrentTransaction}", Transaction.Current);
//logger.Information("{@dbCurrentTransaction}", db.Database.CurrentTransaction);
//db.Database.ExecuteSqlCommand("-- about to save");
db.SaveChanges(); // Try avoid db lock
//db.Database.ExecuteSqlCommand("-- finished save");
//scope.Complete();
}
}
(我说“有点”是因为每个语句 运行 在它们自己的会话中)
也许这是一个很长的说法,是的,即使您无法通过 Profiler 证明它,EF 事务也能正常工作。
根据 ADO.NET 文档 Snapshot Isolation in SQL Server 中的以下注释,只要基础连接被合并,隔离级别就不会绑定到事务范围:
If a connection is pooled, resetting its isolation level does not
reset the isolation level at the server. As a result, subsequent
connections that use the same pooled inner connection start with their
isolation levels set to that of the pooled connection. An alternative
to turning off connection pooling is to set the isolation level
explicitly for each connection.
因此我得出结论,在 SQL Server 2012 之前,将隔离设置为 ReadCommitted
以外的任何其他级别都需要在创建有问题的 SqlConnection 时打开连接池,或者将隔离级别设置为每个连接明确地避免意外行为,包括死锁。或者,可以通过调用 ClearPool Method 来清除连接池,但是由于此方法既未绑定到事务范围也未绑定到底层连接,因此我认为当多个连接 运行 同时反对时它是不合适的相同的池化内部连接。
参考 SQL 论坛中的 post SQL Server 2014 reseting isolation level 和我自己的测试,当使用 SQL Server 2014 和带有 TDS 7.3 的客户端驱动程序时,此类解决方法已过时或更高。
在 MSSQL 2008
上使用 EF 进行查询更新时存在性能和锁定问题。所以我设置了ReadUncommitted事务隔离级别,希望能解决,像这样,
之前
using (MyEntities db = new MyEntities())
{
// large dataset
var data = from _Contact in db.Contact where _Contact.MemberId == 13 select _Contact;
for (var item in data)
item.Flag = 0;
// Probably db lock
db.SaveChanges();
}
之后
using (var scope =
new TransactionScope(TransactionScopeOption.RequiresNew,
new TransactionOptions() { IsolationLevel = IsolationLevel.ReadUncommitted }))
{
using (MyEntities db = new MyEntities())
{
// large dataset but with NOLOCK
var data = from _Contact in db.Contact where _Contact.MemberId == 13 select _Contact;
for (var item in data)
item.Flag = 0;
// Try avoid db lock
db.SaveChanges();
}
}
我们用SQL profiler
来追踪。但是,按顺序获取这些脚本,
(预计第一个脚本未提交读取。)
审核登录
set transaction isolation level read committed
SP:StmtStarting
SELECT
[Extent1].[ContactId] AS [ContactId],
[Extent1].[MemberId] AS [MemberId],
FROM [dbo].[Contact] AS [Extent1]
WHERE [Extent1].[MemberId] = @p__linq__0
审核登录
set transaction isolation level read uncommitted
虽然我可以重新发送这个请求并使其正确排序(将对以下请求显示 read-uncommitted
,相同的 SPID),但我想知道为什么它发送了 read-读取提交命令后未提交的命令以及如何使用 EF 和 TransactionScope 修复?谢谢。
我认为更好的解决方案是通过生成直接查询来执行更新(而不是 selection 并逐个更新实体)。为了使用对象而不是查询,您可以使用 EntityFramework.Extended:
db.Contact.Update(C => c.MemberId == 13, c => new Contact { Flag = 0 });
这应该会生成类似于 UPDATE Contact SET Flag = 0 WHERE MemberId = 13
的内容,它比您当前的解决方案要快得多。
如果我没记错的话,这应该会生成自己的交易。如果这必须在与其他查询的事务中执行,仍然可以使用 `TransactionScope(您将有两个事务)。
此外,隔离级别可以保持不变(ReadCommitted)。
[编辑]
Chris'
的分析准确地显示了发生的事情。为了使其更具相关性,以下代码显示了 TransactionScope
:
using (var db = new myEntities())
{
// this shows ReadCommitted
Console.WriteLine($"Isolation level outside TransactionScope = {db.Database.SqlQuery(typeof(string), selectIsolationLevel).Cast<string>().First()}");
}
using (var scope =
new TransactionScope(TransactionScopeOption.RequiresNew,
new TransactionOptions() { IsolationLevel = IsolationLevel.ReadUncommitted }))
{
// this show ReadUncommitted
Console.WriteLine($"Isolation level inside TransactionScope = {db.Database.SqlQuery(typeof(string), selectIsolationLevel).Cast<string>().First()}");
using (myEntities db = new myEntities ())
{
var data = from _Contact in db.Contact where _Contact.MemberId == 13 select _Contact; // large results but with nolock
for (var item I data)
item.Flag = 0;
db.SaveChanges(); // Try avoid db lock
}
// this should be added to actually Commit the transaction. Otherwise it will be rolled back
scope.Complete();
}
回到实际问题(陷入僵局),如果我们看一下 Profiler 在整个过程中输出的内容,我们会看到类似这样的内容(已删除 GO
s):
BEGIN TRANSACTION
SELECT <all columns> FROM Contact
exec sp_reset_connection
exec sp_executesql N'UPDATE Contact
SET [Flag] = @0
WHERE ([Contact] = @1)
',N'@0 nvarchar(1000),@1 int',@0=N'1',@1=1
-- lots and lots of other UPDATEs like above
-- or ROLLBACK if scope.Complete(); is missed
COMMIT
这有两个缺点:
许多 round-trips - 对数据库发出了很多查询,这给数据库引擎带来了更大的压力,并且客户端也需要更长的时间
长交易 - 应避免长交易作为 minimizing deadlocks
的暂定
因此,建议的解决方案应该更适合您的特定情况(简单更新)。
在更复杂的情况下,可能需要更改隔离级别。
我认为,如果要处理大量数据(select 数百万、做某事、更新回来等),存储过程可能是解决方案,因为所有内容都会执行 server-side。
我认为这是依赖审计登录事件引起的转移注意力。这是 而不是 显示客户端告诉服务器 'set transaction isolation level read uncommitted' 的时刻。当从连接池中取出并重新使用该连接时,它向您展示稍后的隔离级别。
我通过将 Pooling=false
添加到我的连接字符串来验证这一点。然后,审计登录总是显示事务隔离级别读取已提交。
到目前为止,我在 SQL Profiler 中找不到任何方法来查看 EF 设置事务级别的时刻,也没有任何显式 begin tran
.
我可以通过阅读和记录级别来确认它正在某处设置:
const string selectIsolationLevel = @"SELECT CASE transaction_isolation_level WHEN 0 THEN 'Unspecified' WHEN 1 THEN 'ReadUncommitted' WHEN 2 THEN 'ReadCommitted' WHEN 3 THEN 'Repeatable' WHEN 4 THEN 'Serializable' WHEN 5 THEN 'Snapshot' END AS TRANSACTION_ISOLATION_LEVEL FROM sys.dm_exec_sessions where session_id = @@SPID";
static void ReadUncommitted()
{
using (var scope =
new TransactionScope(TransactionScopeOption.RequiresNew,
new TransactionOptions{ IsolationLevel = IsolationLevel.ReadUncommitted }))
using (myEntities db = new myEntities())
{
Console.WriteLine("Read is about to be performed with isolation level {0}",
db.Database.SqlQuery(typeof(string), selectIsolationLevel).Cast<string>().First()
);
var data = from _Contact in db.Contact where _Contact.MemberId == 13 select _Contact; // large results but with nolock
foreach (var item in data)
item.Flag = 0;
//Using Nuget package https://www.nuget.org/packages/Serilog.Sinks.Literate
//logger = new Serilog.LoggerConfiguration().WriteTo.LiterateConsole().CreateLogger();
//logger.Information("{@scope}", scope);
//logger.Information("{@scopeCurrentTransaction}", Transaction.Current);
//logger.Information("{@dbCurrentTransaction}", db.Database.CurrentTransaction);
//db.Database.ExecuteSqlCommand("-- about to save");
db.SaveChanges(); // Try avoid db lock
//db.Database.ExecuteSqlCommand("-- finished save");
//scope.Complete();
}
}
(我说“有点”是因为每个语句 运行 在它们自己的会话中)
也许这是一个很长的说法,是的,即使您无法通过 Profiler 证明它,EF 事务也能正常工作。
根据 ADO.NET 文档 Snapshot Isolation in SQL Server 中的以下注释,只要基础连接被合并,隔离级别就不会绑定到事务范围:
If a connection is pooled, resetting its isolation level does not reset the isolation level at the server. As a result, subsequent connections that use the same pooled inner connection start with their isolation levels set to that of the pooled connection. An alternative to turning off connection pooling is to set the isolation level explicitly for each connection.
因此我得出结论,在 SQL Server 2012 之前,将隔离设置为 ReadCommitted
以外的任何其他级别都需要在创建有问题的 SqlConnection 时打开连接池,或者将隔离级别设置为每个连接明确地避免意外行为,包括死锁。或者,可以通过调用 ClearPool Method 来清除连接池,但是由于此方法既未绑定到事务范围也未绑定到底层连接,因此我认为当多个连接 运行 同时反对时它是不合适的相同的池化内部连接。
参考 SQL 论坛中的 post SQL Server 2014 reseting isolation level 和我自己的测试,当使用 SQL Server 2014 和带有 TDS 7.3 的客户端驱动程序时,此类解决方法已过时或更高。