【问题标题】:Why does this script cause a SQL deadlock often?为什么这个脚本经常导致 SQL 死锁?
【发布时间】:2020-04-08 09:42:12
【问题描述】:

我每小时在 Azure 的 Web 作业中运行一个后台任务。有时(似乎有超过 50% 的时间,代码会在这段特定的代码上阻塞(出现死锁错误):

foreach (var ownerToProcess in activeOwnersWithMessageArchiving)
{
    foreach (var extension in extensions)
    {
        using (var db = new SqlConnection(connectionString))
        {
            db.Execute(@"
UPDATE T_MESSAGESTARTER 
   SET Started=@started,Completed=NULL 
 WHERE OwnerId=@ownerId AND ExtensionId=@extensionId;

if @@ROWCOUNT=0
  INSERT INTO T_MESSAGESTARTER (OwnerId,ExtensionId,Started) 
  VALUES (@ownerId,@extensionId,@started)
", new { ownerId = ownerToProcess, extensionId = extension, started = DateTimeOffset.Now });
        }
    }
}

这是一个简单的更新/插入语句。我“相信”我也在使用行级阻塞。这不在交易中。此外,顶层中大约有 60 个 ownerToProcess 项目。并且其中每一个在内循环中都有 5-60 个 extension 项(在上面的代码中)。这使得在每次运行期间执行大约 4000 次此 SQL 语句。每个 @owner/@extension 组合(在 WHERE 子句中)都是唯一的。

有时它会一直运行而不会出错。但有时我会在 SQL 语句的执行中遇到死锁错误。这可能是什么原因造成的?是因为我在 SQL 语句中有UPDATE/INSERT 结构吗?或者 Dapper 会不会做一些有趣的事情?

另外需要注意的是:T_MESSAGESTARTER 表没有主键。会不会导致这个问题?

【问题讨论】:

  • 我们需要查看您的死锁图以了解哪些资源处于死锁状态以及原因。不是单凭代码就能确定的。
  • 建议:考虑将这两行移到存储过程中,并在存储过程中启用AUTOCOMMIT_SP_MODE=ON。我敢打赌,它很有可能消除僵局。容易做到;易于经验验证;)
  • @DaleK 我不是 SQL 专家,很遗憾我不知道如何生成死锁图。
  • @MattSpinks 如果你在谷歌上获取死锁图,你会发现有一个相当直接的查询来获取它——我只是没有它。

标签: c# sql sql-server dapper database-deadlocks


【解决方案1】:

它不需要是 PK,但 ownerId 和 extensionId 上的复合唯一索引(理想情况下是集群的)将优化更新查询。这可能会通过接触最少的数据来缓解死锁(取决于所涉及的进程)。

【讨论】:

    【解决方案2】:

    即使没有实际更新任何行(当行不存在时),更新也会锁定“表”。 根据并发性,以下最有可能是安全的(如果两个进程永远不会处理相同的所有者和扩展,那么它将起作用)

    IF EXISTS(SELECT ...  FROM T_MESSAGESTARTER WHERE OwnerId=@ownerId....)
    BEGIN
    UPDATE T_MESSAGESTARTER 
       SET Started=@started,Completed=NULL 
     WHERE OwnerId=@ownerId AND ExtensionId=@extensionId;
    END
    ELSE 
      INSERT INTO T_MESSAGESTARTER (OwnerId,ExtensionId,Started) 
      VALUES (@ownerId,@extensionId,@started)
    END
    

    【讨论】:

      【解决方案3】:

      无需在双 foreach 迭代中打开连接。

      using (var db = new SqlConnection(connectionString))
      {
        foreach (var ownerToProcess in activeOwnersWithMessageArchiving)
        {
          foreach (var extension in extensions)
          {
      
                  db.Execute(@"
      UPDATE T_MESSAGESTARTER 
         SET Started=@started,Completed=NULL 
       WHERE OwnerId=@ownerId AND ExtensionId=@extensionId;
      
      if @@ROWCOUNT=0
        INSERT INTO T_MESSAGESTARTER (OwnerId,ExtensionId,Started) 
        VALUES (@ownerId,@extensionId,@started)
      ", new { ownerId = ownerToProcess, extensionId = extension, started = DateTimeOffset.Now });
          }
        }
      }
      

      仅供参考,还存在MERGE 声明。
      这是一个标准的 SQL,也可以做 upserts。

      【讨论】:

        猜你喜欢
        • 2019-12-27
        • 1970-01-01
        • 2012-08-27
        • 1970-01-01
        • 2017-11-16
        • 2020-08-20
        • 1970-01-01
        • 2015-03-09
        • 1970-01-01
        相关资源
        最近更新 更多