触发器上的锁定升级问题

Woo*_*Hoo 6 sql-server-2005 sql-server deadlock

我继承了一个 SQL Server 2005 数据库,该数据库每天出现 2-3 次死锁。

我已将其追踪到白天运行的计划作业,并使用触发器插入到表中。

触发器包含对另一个表的 10 次更新,这些更新的条件略有不同。死锁发生在触发器中。

当一个人提出申请并且工作正在运行时,就会发生死锁。应用程序插入到与预定作业相同的表中。

在此处输入图片说明

从跟踪来看,似乎发生在进程 1 获得键锁,进程 2 获得页锁,然后进程 1 将键锁升级为页锁并且进程 2 尝试获取键锁时发生的情况。

我添加了缺失的索引,这似乎有所帮助,但它仍在发生。我不是 DBA,因此对于解决此问题的方法的任何建议将不胜感激。

我添加了死锁 xml 的链接 - 这是我为复制问题所做的测试。

死锁xml

Str*_*DBA 5

明确的解决方案需要 tableA 的创建脚本,包括所有索引和所涉及的两个 UPDATE 语句的执行计划。话虽如此,我们仍然可以将提供的死锁图与 SQL Server 如何执行更新的知识结合起来,并且很有可能解决这个问题。

此死锁涉及两个 spid 在 tableA、58 和 59 上执行更新。

Spid 58 的查询:

Update b set  syd_id=d.syd_id    
        from tableA b    
        inner join tableC d with (nolock) on b.syd_pers_id=d.syd_pers_id    
        --and b.game='es_lotto'    
        and b.game=d.game  and isnull(b.syd_id,0)=0     
Run Code Online (Sandbox Code Playgroud)

Spid 59 的查询:

Update b set Draw_Date=d.draw_date    
        from tableA b
        inner join tableB d with (nolock) on b.draw_no=d.draw_no    
        and left(b.game,2)='UK' and b.Draw_Date is null     
Run Code Online (Sandbox Code Playgroud)

在这个僵局中,涉及到两个资源:

  1. 索引 IX_tableA_Draw_Date 上的“键锁”。
    • 由 spid 58 拥有,模式 = 'X'(独占)
    • 由 spid 59 请求,模式 = 'U'(更新)
  2. 表A上不同的未指定索引的“页面”锁
    • 由 spid 59 拥有,模式 = 'IX'(意图独占)
    • spid 58 要求,模式 = 'U'(更新)

还使用了 3 种不同的锁所有权模式(U、X、IX):

  1. “U”或更新锁定。从概念上讲,UPDATE 语句的 sql 计划有两个部分:标识要修改的数据的“选择”部分和实际发生修改的“修改”部分。在“选择”部分获取更新锁以保护行不被修改,直到我们真正修改它们(此时获取 X 锁)。更新锁与“S”(共享)锁兼容,因此读者不会被阻塞,但是,它们与 X、IX 和其他 U 锁不兼容。
  2. “X”或独占锁。排他锁用于保护在插入、更新和删除期间被修改的数据。它们与所有其他锁类型不兼容,并确保只有一个作者可以同时访问一个资源。
  3. “IX”或意图排他锁。在较低级别的资源获得 X 锁之前,对较高级别的资源采用意向排他锁。例如,在对行执行 X 锁之前,对关联页和整个表都执行 IX 锁。IX 锁表示 X 锁存在于较低级别。作为锁升级的一部分,它们也可以更改为 X 锁。IX 锁与 X 锁不兼容,因此如果两个事务在一个表上都有 IX 锁,那么两个事务都不能将它们的页锁或行锁升级到表级别。意图锁(包括 IX)是一种优化,它允许 SQL 检查一个更高级别的锁,而不是检查数千个较低级别的锁。

现在,让我们详细检查 spid 58 的 UPDATE 查询。我们从死锁图中知道 spid 58 对 IX_tableA_Draw_Date 中的一行有排他锁。因此,我们知道 syd_id 要么在索引键中,要么在聚集索引键中,要么是一个包含列(我将忽略 spid 58 已经拥有触发器中前一个语句的锁的可能性)。

Spid 58 还试图在不同索引中的页面上获取更新锁。这说明了两件事:

  1. 我们知道,在修改部分开始后,更新的选择部分仍在读取和锁定数据。这意味着没有阻塞运算符影响计划中 tableA 中的行。例如,如果从 tableA 读取行、排序并在 MERGE 连接中使用;然后在获取任何排他锁之前将读取所有行(和 U 锁定)。事实并非如此。
  2. 我们正在尝试在 PAGE 而非 ROW 上获取更新锁,因此我们很可能正在执行索引扫描。

我们还可以假设预计更新的行数相对较少(由于多列连接结合更新插入触发器中的空值或 0,这感觉不像是更新大量行的查询)。考虑到所有这些,很可能 UPDATE 正在使用对我们的神秘索引的扫描(不知道它是集群还是非集群)从 tableA 读取行,并针对 tableC 执行 LOOP JOIN 以获取 tableC.syd_id .

对 spid 59 执行类似的分析,很可能我们正在寻找“Draw_Date 为空”的 IX_tableA_Draw_Date,读取(并在 ROWS 上获取更新锁),对 tableB 进行 LOOP JOIN,然后获取 X ROW 锁并更新我们的“神秘”索引(页面上的 IX 锁意味着行上有 X 锁)。


我们在哪?我们接下来要做什么?
我们知道我们有两个更新语句。两者都在读取和写入同一对索引(读取和写入交换)。在他们已经更新了一些行之后,两者都在等待更新锁。Spid 58 是读取页面,59 是读取行。两者都是写入和锁定行。锁升级不是一个因素,因为行锁 -> 表锁(不是页锁)。

如果 Spid 58 在神秘索引上使用行锁而不是页锁,那么只有当两个查询都更新同一行时才会出现死锁。您可以使用 ROWLOCK 查询提示来实现这一点,但是您会面临将锁升级到表锁的风险,这可能会更糟。

另一种方法是将更新分成两部分。

select id, --Not sure what the PK is...
    syd_id
into #tmp 
from tableA inner join tableB (nolock) on b.syd_pers_id=d.syd_pers_id    
    --and b.game='es_lotto'    
    and b.game=d.game  and isnull(b.syd_id,0)=0

UPDATE tableA SET syd_id = t.syd_id from tableA d inner join #tmp t on t.id = d.id 
Run Code Online (Sandbox Code Playgroud)

这将更改锁定的顺序,因为非聚集索引只会为写入而不是读取锁定。

推荐

死锁是 RDBMS 的自然副产品。即使你做的一切都是“正确的”,你也不能总是消除它们。每天 3-4 次死锁,正确的方法可能是将语句包装在一些错误处理中,以便在发生死锁时重新尝试。

http://msdn.microsoft.com/en-us/library/aa175791(v=sql.80).aspx有一个 SQL 2005 示例。但是,由于这是在触发器中,您可能需要添加错误处理在触发触发器的 INSERT 语句之外。