Entity Framework 4.0 死锁而不是特定场景中的阻塞。如何解决这个问题?
Entity Framework 4.0 Deadlocks instead of blocks in a specific scenario. How to work around this?
我有一个 C# 程序可以写入数据库 table 以进行装运。我在数据库上有另一个进程更新不同的行,以及同一 table 中的不同列(即应付总费用)。费用计算 运行 在后台进行,并根据可能来自其他外部来源的新信息调整费用。
在这些情况下,我的 C# 程序会尝试更新记录,并且可能会命中 PAGE_LOCK
。某些事情偶尔会导致发生锁定。无论哪种方式,我都希望此代码在必要时阻塞,发出提交以便其他项目可以继续处理,但不应死锁。
当我连接 SQL Profiler 时,我可以看到
- 交易开始,
- 进行更新时
- 当异常从数据库返回到 C# 应用程序时。
我已经在代码的注释中记录了这些地方。通过调试,我可以在 100% 的时间内重新创建此事件,方法是停止代码并 运行 在指定的时间更新费用,然后恢复。
TransactionOptions tro = new TransactionOptions();
tro.IsolationLevel = IsolationLevel.Serializable;
tro.Timeout = new TimeSpan(0, 0, timeoutInSeconds);
using (TransactionScope scope =
new TransactionScope(TransactionScopeOption.RequiresNew, tro))
{
// (1) BEGIN TRANSACTION HAPPENS HERE
using (MyEntities ctx = MyCreateContextMethod())
{
// IT READS THE SHIPMENT FROM THE DB HERE. WE DO NOT WANT
// TO READ UNCOMMITTED DATA.
// ------------------------------------------------------------
// SELECT *
// FROM SHIPMENT
// WHERE ShipmentID = 555555666666
// AND STATUS = 'NEW'
// DO SOME VALIDATION LOGIC HERE TO MAKE SURE
// ALL CHANGES MADE ARE PERMITTED. AT _MOST_, 10ms.
// ** DURING THIS WINDOW, FEES STORED PROC RUNS (see below)
if (ValidateShipment(myShipmentRecord))
{
// SHIPMENT IS VALID! WE CAN SAVE THESE CHANGES NOW.
myShipmentRecord.Status = 'VALID';
try
{
// (2) ISSUES "UPDATE Shipment ..."
// (3) DEADLOCK!!!
ctx.SaveChanges();
}
catch (Exception ex)
{
throw new Exception("Error saving Shipment", ex);
}
}
scope.Complete();
}
} // THIS IS WHERE THE COMMIT HAPPENS NORMALLY
fee 命令尝试 运行 数据库,并阻止——这是预期的行为。
-- FEE COMMAND:
-- -------------------------------
BEGIN TRANSACTION
-- BLOCKS HERE, WAITING FOR THE SHIPMENT DB PAGE TO COME UNLOCKED.
-- WHILE NOT OPTIMAL, IT IS ACCEPTABLE.
UPDATE SHIPMENT SET FEE = 123.45
WHERE SHIPMENTID = 123456789
AND STATUS = 'VALID'
COMMIT
我期望发生的是 .NET 代码将发出 UPDATE
并成功,因为它拥有锁(对吗??)。然后它将通过范围的完成和处置发出 COMMIT
,然后 FEE COMMAND 将被取消阻止并正常更新。
实际发生的是 Entity Framework 在 SaveChanges()
处导致死锁,并且 EF 始终被选为死锁受害者。
请注意,费用从不 运行 并在与 C# 代码相同的货件上更新。关于死锁是如何发生的,我唯一的猜测是它锁定了数据库页面,而不是行本身。
我的问题:
如果我要在两个不同的 SSMS 控制台中执行两个不同的事务以模拟相同的行为(运行以完全相同的顺序从 SQL Profiler 中执行实际命令), 事情按预期工作。每个进程在相应的时间阻塞,一切正常进行。为什么在发出SaveChanges()
时Entity Framework会导致死锁?
是否有明确的解决方法?
更新
下面是我 运行 在两个单独的 SSMS windows 中的相同项目。在这种情况下,我什至更新 SAME ROW 只是为了强制行锁依赖性,这不是生产中发生的事情。
WINDOW 1 WINDOW 2
BEGIN TRANSACTION
SELECT TOP (1)
* -- ALL COLUMNS
FROM [dbo].[Shipment] AS [Extent1]
WITH (ROWLOCK, UPDLOCK)
-- ADDED ROWLOCK, UPDLOCK
-- TO MIMIC WHAT IS ACTUALLY
-- HAPPENING IN .NET
WHERE
[Extent1].[ShipmentID] = 55556666
BEGIN TRANSACTION
UPDATE shp
SET Fee = 123.45
FROM Shipment shp
WHERE shp.ShipmentID = 55556666
-- This blocks, as expected
UPDATE [dbo].[Shipment]
SET
[ShipmentStatusID] = 30,
[LastUpdateUser] = 'SomeUser'
where ([ShipmentID] = 55556666)
COMMIT -- AT THIS MOMENT, WINDOW 2 IS UNBLOCKED
COMMIT
所有这些 运行 都符合预期,没有死锁。
我发现了问题。我的缺陷出现在我选择的 IsolationLevel 中:
TransactionOptions tro = new TransactionOptions();
tro.IsolationLevel = IsolationLevel.Serializable; // <--- This is the culprit
tro.Timeout = new TimeSpan(0, 0, timeoutInSeconds);
Serializable 将在几个不同的级别锁定记录,并且对于我需要的 IsolationLevel 来说太高了。让我明白的是,当我编写 SQL 查询时,我必须强制执行 UPDLOCK 和 ROWLOCK,假设它们与 Serializable
相同。不幸的是,Serializable
不仅仅是 ROWLOCK 和 UPDLOCK。
在对锁、死锁和隔离级别进行大量阅读之后,我确定我真正需要的只是 ReadCommitted
。当我更改为此 IsolationLevel 时,事情的表现完全符合我的预期,并且没有导致死锁。
这个故事的寓意:Serializable 是一个特定用例的 IsolationLevel。谨慎使用。
我有一个 C# 程序可以写入数据库 table 以进行装运。我在数据库上有另一个进程更新不同的行,以及同一 table 中的不同列(即应付总费用)。费用计算 运行 在后台进行,并根据可能来自其他外部来源的新信息调整费用。
在这些情况下,我的 C# 程序会尝试更新记录,并且可能会命中 PAGE_LOCK
。某些事情偶尔会导致发生锁定。无论哪种方式,我都希望此代码在必要时阻塞,发出提交以便其他项目可以继续处理,但不应死锁。
当我连接 SQL Profiler 时,我可以看到
- 交易开始,
- 进行更新时
- 当异常从数据库返回到 C# 应用程序时。
我已经在代码的注释中记录了这些地方。通过调试,我可以在 100% 的时间内重新创建此事件,方法是停止代码并 运行 在指定的时间更新费用,然后恢复。
TransactionOptions tro = new TransactionOptions();
tro.IsolationLevel = IsolationLevel.Serializable;
tro.Timeout = new TimeSpan(0, 0, timeoutInSeconds);
using (TransactionScope scope =
new TransactionScope(TransactionScopeOption.RequiresNew, tro))
{
// (1) BEGIN TRANSACTION HAPPENS HERE
using (MyEntities ctx = MyCreateContextMethod())
{
// IT READS THE SHIPMENT FROM THE DB HERE. WE DO NOT WANT
// TO READ UNCOMMITTED DATA.
// ------------------------------------------------------------
// SELECT *
// FROM SHIPMENT
// WHERE ShipmentID = 555555666666
// AND STATUS = 'NEW'
// DO SOME VALIDATION LOGIC HERE TO MAKE SURE
// ALL CHANGES MADE ARE PERMITTED. AT _MOST_, 10ms.
// ** DURING THIS WINDOW, FEES STORED PROC RUNS (see below)
if (ValidateShipment(myShipmentRecord))
{
// SHIPMENT IS VALID! WE CAN SAVE THESE CHANGES NOW.
myShipmentRecord.Status = 'VALID';
try
{
// (2) ISSUES "UPDATE Shipment ..."
// (3) DEADLOCK!!!
ctx.SaveChanges();
}
catch (Exception ex)
{
throw new Exception("Error saving Shipment", ex);
}
}
scope.Complete();
}
} // THIS IS WHERE THE COMMIT HAPPENS NORMALLY
fee 命令尝试 运行 数据库,并阻止——这是预期的行为。
-- FEE COMMAND:
-- -------------------------------
BEGIN TRANSACTION
-- BLOCKS HERE, WAITING FOR THE SHIPMENT DB PAGE TO COME UNLOCKED.
-- WHILE NOT OPTIMAL, IT IS ACCEPTABLE.
UPDATE SHIPMENT SET FEE = 123.45
WHERE SHIPMENTID = 123456789
AND STATUS = 'VALID'
COMMIT
我期望发生的是 .NET 代码将发出 UPDATE
并成功,因为它拥有锁(对吗??)。然后它将通过范围的完成和处置发出 COMMIT
,然后 FEE COMMAND 将被取消阻止并正常更新。
实际发生的是 Entity Framework 在 SaveChanges()
处导致死锁,并且 EF 始终被选为死锁受害者。
请注意,费用从不 运行 并在与 C# 代码相同的货件上更新。关于死锁是如何发生的,我唯一的猜测是它锁定了数据库页面,而不是行本身。
我的问题:
如果我要在两个不同的 SSMS 控制台中执行两个不同的事务以模拟相同的行为(运行以完全相同的顺序从 SQL Profiler 中执行实际命令), 事情按预期工作。每个进程在相应的时间阻塞,一切正常进行。为什么在发出
SaveChanges()
时Entity Framework会导致死锁?是否有明确的解决方法?
更新
下面是我 运行 在两个单独的 SSMS windows 中的相同项目。在这种情况下,我什至更新 SAME ROW 只是为了强制行锁依赖性,这不是生产中发生的事情。
WINDOW 1 WINDOW 2
BEGIN TRANSACTION
SELECT TOP (1)
* -- ALL COLUMNS
FROM [dbo].[Shipment] AS [Extent1]
WITH (ROWLOCK, UPDLOCK)
-- ADDED ROWLOCK, UPDLOCK
-- TO MIMIC WHAT IS ACTUALLY
-- HAPPENING IN .NET
WHERE
[Extent1].[ShipmentID] = 55556666
BEGIN TRANSACTION
UPDATE shp
SET Fee = 123.45
FROM Shipment shp
WHERE shp.ShipmentID = 55556666
-- This blocks, as expected
UPDATE [dbo].[Shipment]
SET
[ShipmentStatusID] = 30,
[LastUpdateUser] = 'SomeUser'
where ([ShipmentID] = 55556666)
COMMIT -- AT THIS MOMENT, WINDOW 2 IS UNBLOCKED
COMMIT
所有这些 运行 都符合预期,没有死锁。
我发现了问题。我的缺陷出现在我选择的 IsolationLevel 中:
TransactionOptions tro = new TransactionOptions();
tro.IsolationLevel = IsolationLevel.Serializable; // <--- This is the culprit
tro.Timeout = new TimeSpan(0, 0, timeoutInSeconds);
Serializable 将在几个不同的级别锁定记录,并且对于我需要的 IsolationLevel 来说太高了。让我明白的是,当我编写 SQL 查询时,我必须强制执行 UPDLOCK 和 ROWLOCK,假设它们与 Serializable
相同。不幸的是,Serializable
不仅仅是 ROWLOCK 和 UPDLOCK。
在对锁、死锁和隔离级别进行大量阅读之后,我确定我真正需要的只是 ReadCommitted
。当我更改为此 IsolationLevel 时,事情的表现完全符合我的预期,并且没有导致死锁。
这个故事的寓意:Serializable 是一个特定用例的 IsolationLevel。谨慎使用。