【问题标题】:Deadlock with EF 6 entity update but not ExecuteSqlCommandEF 6 实体更新但不是 ExecuteSqlCommand 的死锁
【发布时间】:2020-11-15 04:52:37
【问题描述】:

在我的数据库中处理并发:

  1. 客户端 A 更新一行
  2. 客户端 B 尝试更新同一行
  3. 客户端 B 需要等待客户端 A 提交他的更新

客户端 A 和 B 实例都被模拟并使用此代码:

 using (myEntities db = new myEntities ())
 {
     db.Database.Connection.Open();

     try
     {
         using (var scope = db .Database.BeginTransaction(System.Data.IsolationLevel.Serializable))
         {
             {  
                 var test = db.customer_table.Where(x => x.id == 38).FirstOrDefault();
                 test.bank_holder_name = "CLIENT NAME XXXX";
                 db.SaveChanges(); <=== CLIENT B stop here while client A still in progress. After CLIENT A finish commit, here will throw *Deadlock found error*"
                 scope.Commit();
             }
         }
     }
     catch (Exception ex)
     {
         throw;
     }
 }

这不是我所期望的 Client B 应该等待并且不允许查询有关行 id=38 的任何数据,但不知何故它可以继续直到 SaveChanges 并最终抛出错误.

因此,我怀疑这可能是由 linq(不正确的行/表锁)引起的

我编辑了我的代码如下:

 using (myEntities db = new myEntities ())
 {
     db.Database.Connection.Open();

     try
     {
         using (var scope = db .Database.BeginTransaction(System.Data.IsolationLevel.Serializable))
         {
             {  
                 var test = db.Database.ExecuteSqlCommand("Update customer_table set bank_holder_name = 'CLIENT XXXXX' where pu_id = 38"); <===== Client B is stop here and proceed after Client A is completed
                 db.SaveChanges();
                 scope.Commit();
             }
         }
     }
     catch (Exception ex)
     {
         throw;
     }
 }

最后,事务正在使用上面的代码(不是 linq 函数)。这太令人困惑了,linq 在使 Transaction 工作不一致的行为背后做了什么?

【问题讨论】:

  • 隔离级别没有定义/规定锁定时刻。这是事务逻辑行为的指示:sqlperformance.com/2014/04/t-sql-queries/…
  • 并且在 SQL Server 中 SERIALIZABLE 允许并发读取行,但通过使一个事务失败并出现死锁来防止冲突更新。
  • 好吧,看来我完全误解了 SQL Server 中的事务隔离。如果是这种情况,我们如何处理 EF 中上述情况的并发?我相信常见的解决方案是使用悲观锁,但 EF 不支持这个....

标签: sql entity-framework linq isolation pessimistic-locking


【解决方案1】:

这是由于 EF 代码生成了两条 SQL 语句:SELECT 用于该行:

var test = db.customer_table.Where(x => x.id == 38).FirstOrDefault();

...以及后续的UPDATE 用于SaveChanges() 调用。

SELECT 语句运行时,客户端A 和客户端B 使用可序列化的隔离级别在记录上的事务期间都使用共享锁。然后,当他们中的一个或其他第一次尝试执行UPDATE 时,他们无法获得必要的排他锁,因为另一个客户端上有一个共享锁。然后另一个客户端本身尝试获取排他锁,而您遇到了死锁情况。

ExecuteSqlCommand 只需要一个更新语句,因此不会发生死锁。

Serializable 隔离级别可以大大降低并发性,这个例子说明了原因。您会发现不太严格的隔离级别将允许 EF 代码工作,但存在幻像记录、不可重复读取等风险。然而,这些可能是您愿意承担和/或减轻的风险,以便提高并发性。

【讨论】:

  • 您好,strict01,感谢您的通知。你怎么知道EF选择代码下有2条SQL语句?
  • @Tsushima 因为您正在使用 DbContext 实体 customer_table 填充 test。为此,EF 将转到数据库以获取值。那么SaveChanges调用会触发SQLUPDATE
【解决方案2】:

不要先获取实体。而是创建一个“存根实体”并更新它,例如

var test = new Customer() { id = 38 };
test.bank_holder_name = "CLIENT NAME XXXX";
db.Entry(test).Property(nameof(Customer.bank_holder_name)).IsModified = true;
db.SaveChanges();

翻译成

  SET NOCOUNT ON;
  UPDATE [Customers] SET [bank_holder_name] = @p0
  WHERE [id] = @p1;
  SELECT @@ROWCOUNT;

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-06-03
    • 2022-12-02
    • 2021-10-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-02-04
    相关资源
    最近更新 更多