【问题标题】:Deadlock error on same table with two Update SQL statements使用两个更新 SQL 语句在同一张表上出现死锁错误
【发布时间】:2018-08-05 10:22:09
【问题描述】:

我有一个将数据写入 TSQL 数据库的 C# 项目。有两个更新语句在循环中运行,例如:

 for (int i = 0; i < customersProducts.Count; i++) {
     CustomerProducts c = customersProducts[i];

     // Update product dimensions
     for (int j = 0; j < c.Count; j++) {
         Product p = c[j];
         updateProductDimensions(p);
     }

     // ... some processing

     // Update product
     for (int j = 0; j < c.Count; j++) {
         Product p = c[j];
         updateProduct(p);
     }
 }

updateProductDimensions()updateProduct() 都触发 SQL 更新语句。更新的列有一些重叠:

string updateProductDimensions = "UPDATE products SET width = @width, height = @height, length = @length WHERE id = @id";
string updateProduct = "UPDATE products SET width = @width, height = @height, length = @length, customer_id = @customer_id, weight = @weight .... WHERE id = @id";

updateProductDimensions() 方法示例 - updateProduct() 也类似:

public void updateProductDimensions(Product p) {
     SqlConnection connection = DBFactory.getConnection();
     string updateProductDimensions = "UPDATE products SET width = @width, height = @height, length = @length WHERE id = @id";

     try
     {
         SqlCommand sqlCmd = new sqlCmd(updateProductDimensions, connection);
         sqlCmd.Parameters.AddWithValue("@width", 20);
         sqlCmd.Parameters.AddWithValue("@height", 10);
         sqlCmd.Parameters.AddWithValue("@length", 30);
         sqlCmd.Parameters.AddWithValue("@id", p.id);
         sqlCmd.CommandType = CommandType.Text;

         sqlCmd.ExecuteNonQuery();

     }
     catch (Exception e)
     {
         // Handle exception
     }
     finally
     {
         connection.Close();
     }
}

我运行了 SQL Server 死锁跟踪,它显示 updateProduct 语句失败(即受害进程),而幸存的进程是运行 updateProductDimensions 语句的进程。

死锁跟踪的简化版本如下(最近的进程在前):

 - updateProduct2: fail
 - updateProduct2: success
 - updateProduct1: success
 - updateProductDimensions4: success
 - updateProductDimensions3: success
 - updateProductDimensions2: success
 - updateProductDimensions1: success

每一行代表每个 for loop 迭代更新的一个产品。

还有updateProduct2 的资源/所有者列表:

  - owner: updateProductDimensions1 (mode = U, isolationLevel = read committed (2))
  - waiter: updateProduct2 (mode= U, requestType = wait, isolationLevel = read committed (2))

我的问题是,为什么会发生死锁?即使这两个语句更新同一行,它也是同一个表。服务器与多个客户端通信,其中客户端只能更新他们自己的产品 - 即。一个产品只能由一个特定的客户更新。这样,多个数据库更新同时发生,但针对不同的行(产品)。

如何在不删除重复的更新列的情况下解决此问题?


products 表创建语句:

    CREATE TABLE Products (
        [id]               VARCHAR (255)    NOT NULL,
        [width]            INT              NOT NULL,
        [length]           INT              NOT NULL,
        [height]           INT              NOT NULL,
        [weight]           INT              NOT NULL,
        // more fields
        [customer_id]      INT                  CONSTRAINT [F_KEY_CUSTOMER] DEFAULT ((0)) NOT NULL,
        CONSTRAINT [P_KEY_PRODUCT] PRIMARY KEY CLUSTERED ([id] ASC),
        CONSTRAINT [F_KEY_CUSTOMER] FOREIGN KEY ([customer_id]) REFERENCES [dbo].[Customer] ([id])
    );

查询计划

更新产品尺寸声明:

更新产品声明:


死锁跟踪

    <TextData>
    <deadlock-list>
    <deadlock victim="victimProcess">
    <process-list>
    <process id="victimProcess" taskpriority="0" logused="0" waitresource="PAGE: 15:1:1259" waittime="4594" ownerId="21610772296" transactionname="UPDATE" lasttranstarted="2018-02-21T08:46:44.777" XDES="0x859b9c580" lockMode="U" schedulerid="20" kpid="34240" status="suspended" spid="64" sbid="3" ecid="3" priority="0" trancount="0" lastbatchstarted="2018-02-21T08:46:44.777" lastbatchcompleted="2018-02-21T08:46:44.777" clientapp=".Net SqlClient Data Provider" hostname="" hostpid="636" isolationlevel="read committed (2)" xactid="21610772296" currentdb="15" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
        <executionStack>
        <frame procname="adhoc" line="1" stmtstart="422" sqlhandle="0x02000000696bc4026d3a5eb5fc3835e32324ce9f3e4bdd28">
    UPDATE products SET width = @width, height = @height, length = @length, customer_id = @customer_id, weight = @weight WHERE id = @id     </frame>
        <frame procname="unknown" line="1" sqlhandle="0x000000000000000000000000000000000000000000000000">
    unknown     </frame>
        </executionStack>
        <inputbuf>
        </inputbuf>
    </process>
    <process id="survivorProcess4" taskpriority="0" logused="0" waitresource="PAGE: 15:1:2795" waittime="4593" ownerId="21610772296" transactionname="UPDATE" lasttranstarted="2018-02-21T08:46:44.777" XDES="0x45ebe3ca0" lockMode="U" schedulerid="18" kpid="254204" status="suspended" spid="64" sbid="3" ecid="6" priority="0" trancount="0" lastbatchstarted="2018-02-21T08:46:44.777" lastbatchcompleted="2018-02-21T08:46:44.777" clientapp=".Net SqlClient Data Provider" hostname="" hostpid="636" isolationlevel="read committed (2)" xactid="21610772296" currentdb="15" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
        <executionStack>
        <frame procname="adhoc" line="1" stmtstart="422" sqlhandle="0x02000000696bc4026d3a5eb5fc3835e32324ce9f3e4bdd28">
    UPDATE products SET width = @width, height = @height, length = @length, customer_id = @customer_id, weight = @weight WHERE id = @id     </frame>
        <frame procname="unknown" line="1" sqlhandle="0x000000000000000000000000000000000000000000000000">
    unknown     </frame>
        </executionStack>
        <inputbuf>
        </inputbuf>
    </process>
    <process id="survivorProcess3" taskpriority="0" logused="224" waitresource="PAGE: 15:1:2795" waittime="4527" ownerId="21610772095" transactionname="UPDATE" lasttranstarted="2018-02-21T08:46:44.680" XDES="0x859b9c300" lockMode="U" schedulerid="20" kpid="16324" status="suspended" spid="123" sbid="2" ecid="1" priority="0" trancount="0" lastbatchstarted="2018-02-21T08:46:44.680" lastbatchcompleted="2018-02-21T08:46:44.673" clientapp=".Net SqlClient Data Provider" hostname="" hostpid="636" isolationlevel="read committed (2)" xactid="21610772095" currentdb="15" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
        <executionStack>
        <frame procname="adhoc" line="1" stmtstart="102" sqlhandle="0x020000007e9c95155af7dd6044d8697705c48a1d5856dba4">
    UPDATE products SET width = @width, height = @height, length = @length WHERE id = @id     </frame>
        <frame procname="unknown" line="1" sqlhandle="0x000000000000000000000000000000000000000000000000">
    unknown     </frame>
        </executionStack>
        <inputbuf>
        </inputbuf>
    </process>
    <process id="survivorProcess2" taskpriority="0" logused="224" waitresource="PAGE: 15:1:1259" waittime="4529" ownerId="21610772095" transactionname="UPDATE" lasttranstarted="2018-02-21T08:46:44.680" XDES="0x270bf8b20" lockMode="U" schedulerid="13" kpid="406864" status="suspended" spid="123" sbid="2" ecid="4" priority="0" trancount="0" lastbatchstarted="2018-02-21T08:46:44.680" lastbatchcompleted="2018-02-21T08:46:44.673" clientapp=".Net SqlClient Data Provider" hostname="" hostpid="636" isolationlevel="read committed (2)" xactid="21610772095" currentdb="15" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
        <executionStack>
        <frame procname="adhoc" line="1" stmtstart="102" sqlhandle="0x020000007e9c95155af7dd6044d8697705c48a1d5856dba4">
    UPDATE products SET width = @width, height = @height, length = @length WHERE id = @id     </frame>
        <frame procname="unknown" line="1" sqlhandle="0x000000000000000000000000000000000000000000000000">
    unknown     </frame>
        </executionStack>
        <inputbuf>
        </inputbuf>
    </process>
    <process id="survivorProcess1" taskpriority="0" logused="10000" waittime="4315" schedulerid="17" kpid="30464" status="suspended" spid="123" sbid="2" ecid="0" priority="0" trancount="2" lastbatchstarted="2018-02-21T08:46:44.680" lastbatchcompleted="2018-02-21T08:46:44.673" clientapp=".Net SqlClient Data Provider" hostname="" hostpid="636" loginname="" isolationlevel="read committed (2)" xactid="21610772095" currentdb="15" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
        <executionStack>
        <frame procname="adhoc" line="1" stmtstart="102" sqlhandle="0x020000007e9c95155af7dd6044d8697705c48a1d5856dba4">
    UPDATE products SET width = @width, height = @height, length = @length WHERE id = @id     </frame>
        <frame procname="unknown" line="1" sqlhandle="0x000000000000000000000000000000000000000000000000">
    unknown     </frame>
        </executionStack>
        <inputbuf>
    (@width int,@height int,@length int,@id nvarchar(255))UPDATE products SET width = @width, height = @height, length = @length WHERE id = @id    </inputbuf>
    </process>
    </process-list>
    <resource-list>
    <pagelock fileid="1" pageid="1259" dbid="15" objectname="MyDB.dbo.Product" id="lock15a855b00" mode="U" associatedObjectId="72057594038845440">
        <owner-list>
        <owner id="survivorProcess1" mode="U" />
        </owner-list>
        <waiter-list>
        <waiter id="victimProcess" mode="U" requestType="wait" />
        </waiter-list>
    </pagelock>
    <pagelock fileid="1" pageid="2795" dbid="15" objectname="MyDB.dbo.Product" id="lockbb9f0f80" mode="U" associatedObjectId="72057594038845440">
        <owner-list>
        <owner id="survivorProcess1" mode="U" />
        </owner-list>
        <waiter-list>
        <waiter id="survivorProcess4" mode="U" requestType="wait" />
        </waiter-list>
    </pagelock>
    <pagelock fileid="1" pageid="2795" dbid="15" objectname="MyDB.dbo.Product" id="lockbb9f0f80" mode="U" associatedObjectId="72057594038845440">
        <owner-list />
        <waiter-list>
        <waiter id="survivorProcess3" mode="U" requestType="wait" />
        </waiter-list>
    </pagelock>
    <pagelock fileid="1" pageid="1259" dbid="15" objectname="MyDB.dbo.Product" id="lock15a855b00" mode="U" associatedObjectId="72057594038845440">
        <owner-list />
        <waiter-list>
        <waiter id="survivorProcess2" mode="U" requestType="wait" />
        </waiter-list>
    </pagelock>
    <exchangeEvent id="Pipe49e4ca380" WaitType="e_waitPipeGetRow" nodeId="2">
        <owner-list>
        <owner id="survivorProcess3" />
        <owner id="survivorProcess2" />
        </owner-list>
        <waiter-list>
        <waiter id="survivorProcess1" />
        </waiter-list>
    </exchangeEvent>
    </resource-list>
    </deadlock>
    </deadlock-list>
    </TextData>

【问题讨论】:

  • 您的应用程序是否只有一个实例在运行?
  • 我可以知道你为什么这样做吗?看来一次更新对我来说就足够了。
  • 如果发生死锁,那么涉及一些并行性。
  • @user2181948,答案几乎肯定会在所持有的冲突锁的其他详细信息中找到。
  • 听起来您有一个业务案例来更新同一张表上的两组不同的列。每个表有一个更新会很好。由于您的 where 子句针对聚集索引,因此更多索引可能无济于事。查看每个查询的查询计划通常是一个不错的举措。作为回退,添加 SQL 用户锁可以根据您定义的锁名称序列化请求,这会序列化访问,从而降低并行度。

标签: c# sql tsql deadlock database-deadlocks


【解决方案1】:

这个问题没有包含足够的场景让我能够复制这个例子,所以我要推测一下。

SqlCommand 是一次性的;但不在 using 块中,也没有被释放,所以我怀疑当后续命令发生时,前面的命令仍在干扰数据库。

将两个 SqlCommand 放入“使用”块中;当您使用它时,删除“finally{connection.Close();}”,并将 SqlConnection 也放入“使用”块中(Dispose 将执行关闭)。

【讨论】:

  • 我感觉就是这样,对于实现 IDisposable 且未包含在 using 或显式调用 Dispose 的任何内容,都应该有一个编译警告。
  • 这不是问题的一部分(还),但我还将提供指向Can we stop using AddWithValue 的强制性链接作为修复此代码的奖励。
  • Disposing SqlCommand 不执行任何 DB 相关操作,而是在客户端释放资源。您绝对应该处置(关闭)连接,关于 SqlCommand,这是 ADO.NET 指南所说的:“虽然您可以重复使用相同的 SqlCommand 对象多次执行相同的命令,但不要重复使用相同的 SqlCommand 对象来执行不同的命令。" bytes.com/topic/c-sharp/answers/…
【解决方案2】:

现在已经确定,死锁是“复杂的野兽”!

理论上,由于您正在更新单个表,因此没有涉及 2 个表来解释“经典死锁”场景。所以,你会期望不会出现死锁,但你得到了它!欢迎来到现实世界:-)

根据您的死锁跟踪 xml,您似乎由于“Page Lock”而出现死锁 即,SQL Server 正在锁定一个页面,而您的进程在一个页面上死锁(即,不仅仅是您的记录)。

如果您查看死锁跟踪的resource-list 部分,您可以看到您的受害进程正在等待被另一个进程锁定的页面。

您可以尝试的一种简单技术是在更新语句中使用ROWLOCK 提示,看看这是否对您的方案有帮助。

相关的 SO 帖子: https://dba.stackexchange.com/questions/121610/how-to-force-sql-server-to-use-row-locking-for-specific-update-delete-statements

UPDATE Table1 WITH (ROWLOCK)
SET FirstName = 'first'
WHERE ID = 1

在上面的例子中WITH (ROWLOCK)是你的hint到SQL服务器尝试使用行级锁

此外,关于 SQL 服务器死锁的一些好读物是在这个简单的谈话link

【讨论】:

    【解决方案3】:

    根据情况,MSSQL 服务器会锁定整个页面(多行)而不是单行。这就是为什么即使每个客户端只访问它自己的行也会发生死锁的原因。我也经历过假死锁,即非常繁忙的服务器真正超时。

    1) 告诉 SQL Server 使用行锁定(而不是页面锁定)。这可能会影响性能。

     UPDATE products WITH (ROWLOCK) SET ...
    

    2) 确保在 where 条件中使用主键。

    与您的死锁问题无关:

    3) 您有嵌套循环,您可以在其中将单个语句触发到 MSSQL。通过构建一两个大语句来减少查询的数量。这应该会提高您的运行时性能

    4) 处理您的 SqlConnection 和 SqlCommand。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-01-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多