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 时,我可以看到

  1. 交易开始,
  2. 进行更新时
  3. 当异常从数据库返回到 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 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。谨慎使用。