实体框架4.0死锁,而不是块在一个特定的场景.如何解决这个问题

本文关键字:何解决 问题 解决 一个 框架 死锁 实体 | 更新日期: 2023-09-27 18:04:18

我有一个c#程序,它向数据库表写入货物。我在数据库上有另一个进程,更新不同的行,不同的列在同一表(即总费用到期)。费用计算在后台运行,并根据可能来自其他外部来源的新信息调整费用。

在这些情况下,我的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命令尝试在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

实体框架4.0死锁,而不是块在一个特定的场景.如何解决这个问题

我发现了这个问题。我的缺陷在于我选择的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。请谨慎使用