实体框架4.0死锁,而不是块在一个特定的场景.如何解决这个问题
本文关键字:何解决 问题 解决 一个 框架 死锁 实体 | 更新日期: 2023-09-27 18:04:18
我有一个c#程序,它向数据库表写入货物。我在数据库上有另一个进程,更新不同的行,不同的列在同一表(即总费用到期)。费用计算在后台运行,并根据可能来自其他外部来源的新信息调整费用。
在这些情况下,我的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命令尝试在DB上运行,并阻塞——这是预期的行为。
-- 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命令将被解除阻塞并正常更新。
实际发生的情况是实体框架在SaveChanges()
处导致死锁,EF总是被选为死锁受害者。
注意,费用永远不会在与c#代码相同的装运上运行和更新。关于死锁是如何发生的,我唯一的猜测是它锁定了DB页,而不是行本身。
我的问题:
如果我要在两个不同的SSMS控制台执行两个不同的事务来模拟相同的行为(以完全相同的顺序运行SQL Profiler中的实际命令),那么事情就会像预期的那样工作。每个进程在相应的时间阻塞,一切正常进行。
为什么实体框架在发出是否有明确的解决方案?
SaveChanges()
时会导致死锁?下面是我在两个独立的SSMS窗口中运行的相同项目。在这种情况下,我甚至更新相同的行只是为了强制行锁依赖,这不是在生产中发生的。
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。请谨慎使用