在MSSQL 2008上使用EF进行查询更新时存在性能和锁定问题。所以我把 ReadUncommitted 事务隔离级别,希望解决它,就像这样,
之前
using (myEntities db = new myEntities())
{
var data = from _Contact in db.Contact where _Contact.MemberId == 13 select _Contact; // large results
for (var item I data)
item.Flag = 0;
db.SaveChanges(); // Probably db lock
}
后
using (var scope =
new TransactionScope(TransactionScopeOption.RequiresNew,
new TransactionOptions() { IsolationLevel = IsolationLevel.ReadUncommitted }))
{
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
}
}
我们使用SQL分析器进行确认。但是,按顺序获取这些脚本, (第一个脚本的 Expect read-uncommitted 。)
审核登录
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
虽然我可以重新发送此请求并使其顺序正确(将显示以下请求的读取 - 未提交,相同的SPID),我想知道为什么它在read-committed命令之后发送了read-uncommitted命令以及如何使用EF和TransactionScope?感谢。
答案 0 :(得分:4)
我认为这是依赖审核登录事件造成的红鲱鱼。这是不显示客户端告诉服务器设置事务隔离级别读取未提交的时刻'。它向您展示了以后的隔离级别,何时从池中挑选出该连接并重新使用。
我通过将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交易也能正常运作。
答案 1 :(得分:3)
根据ADO.NET文档Snapshot Isolation in SQL Server中的以下注释,只要底层连接被合并,隔离级别就不会绑定到事务范围:
如果汇集了连接,则不会重置其隔离级别 重置服务器上的隔离级别。结果,随后 使用相同的池内连接的连接从它们开始 隔离级别设置为池连接的级别。替代 关闭连接池是设置隔离级别 明确地为每个连接。
因此,我得出结论,在SQL Server 2012之前,将隔离设置为ReadCommitted
以外的任何其他级别都需要在创建可疑SqlConnection时关闭连接池,或者明确设置每个连接中的隔离级别以避免意外行为,包括死锁。或者可以通过调用ClearPool Method来清除连接池,但由于此方法既不绑定到事务范围也不绑定到底层连接,所以当多个连接同时针对同一个内部连接运行时,我不认为它是合适的连接。
引用SQL论坛中的帖子SQL Server 2014 reseting isolation level和我自己的测试,当使用SQL Server 2014和TDS 7.3或更高版本的客户端驱动程序时,这些变通办法已经过时。
答案 2 :(得分:-1)
我认为更好的解决方案是通过生成直接查询来执行更新(而不是按实体选择和更新实体)。要使用对象而不是查询,可以使用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
这有两个缺点:
许多往返 - 针对数据库发出了许多查询,这给数据库引擎带来了更大的压力,并且对客户端也需要更长的时间
长期交易 - 应避免长期交易作为minimizing deadlocks的暂定
因此,建议的解决方案应该在您的特定情况下更好地工作(简单更新)。
在更复杂的情况下,可能需要更改隔离级别。
我认为,如果处理大量数据处理(选择数百万,做某事,更新等),存储过程可能就是解决方案,因为一切都在服务器端执行。