【问题标题】:Why does a select query require an IX lock?为什么选择查询需要 IX 锁?
【发布时间】:2017-01-31 08:48:25
【问题描述】:

在检查下面的死锁图时,我发现一个SELECT 查询(仅在SP 内由第一个进程process569f048 执行的查询)和一个UPDATE 查询形成了死锁; SELECT 查询需要 IX 锁定。

在什么情况下SELECT 需要这种锁?我该怎么做才能避免死锁?

这里是SELECT 查询:

SELECT TOP (@p_takeCount)
     t.Id
    ,s.Column2
    ,t.STATUS
    ,t.Column3
    ,t.Column4
FROM Table2 t WITH (INDEX (IX_Table2))
INNER JOIN Table1 s ON s.Id = t.ParentId
WHERE t.STATUS != 0
    AND t.Column5 IS NULL
    AND s.SomeId = @p_someId
    AND s.Category = 2
ORDER BY t.id

计划如下:

这是UPDATE 查询:

update Table2
set [Status] = @0, Column5 = null, Column6 = @1
where ([Id] = @2)

计划如下:

这是死锁图:

<deadlock>
  <victim-list>
    <victimProcess id="process569f048" />
  </victim-list>
  <process-list>
    <process id="process569f048" taskpriority="0" logused="0" waitresource="PAGE: 5:1:3017144" waittime="2867" ownerId="964271246" transactionname="SELECT" lasttranstarted="2017-01-29T10:10:49.643" XDES="0x800f9d20" lockMode="S" schedulerid="10" kpid="10108" status="suspended" spid="70" sbid="2" ecid="2" priority="0" trancount="0" lastbatchstarted="2017-01-29T10:10:49.643" lastbatchcompleted="2017-01-29T10:10:49.643" clientapp="EntityFramework" hostname="LOCALHOST" hostpid="4936" isolationlevel="read committed (2)" xactid="964271246" currentdb="5" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
      <executionStack>
        <frame procname="" line="17" stmtstart="1298" stmtend="1954" sqlhandle="0x03000500d21f5e3dd6d19700cca400000100000000000000" />
      </executionStack>
      <inputbuf />
    </process>
    <process id="process8ee3dc8" taskpriority="0" logused="17956" waitresource="PAGE: 5:1:3017343" waittime="2864" ownerId="964271345" transactionname="user_transaction" lasttranstarted="2017-01-29T10:10:49.667" XDES="0xafdbb03b0" lockMode="IX" schedulerid="17" kpid="9468" status="suspended" spid="61" sbid="2" ecid="0" priority="0" trancount="2" lastbatchstarted="2017-01-29T10:10:49.703" lastbatchcompleted="2017-01-29T10:10:49.703" clientapp="EntityFramework" hostname="LOCALHOST" hostpid="20696" loginname="dbuser_d" isolationlevel="read committed (2)" xactid="964271345" currentdb="5" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
      <executionStack>
        <frame procname="" line="1" stmtstart="74" sqlhandle="0x02000000403aaa03bd8879de1c73d49641f1f81b6ca095af" />
        <frame procname="" line="1" sqlhandle="0x000000000000000000000000000000000000000000000000" />
      </executionStack>
      <inputbuf>
        (@0 tinyint,@1 varchar(64),@2 bigint)update [dbo].[Table2]
        set [Status] = @0, [Column5] = null, [Column6] = @1
        where ([Id] = @2)
      </inputbuf>
    </process>
  </process-list>
  <resource-list>
    <pagelock fileid="1" pageid="3017144" dbid="5" objectname="" id="lockc296c6380" mode="IX" associatedObjectId="72057594073317376">
      <owner-list>
        <owner id="process8ee3dc8" mode="IX" />
      </owner-list>
      <waiter-list>
        <waiter id="process569f048" mode="S" requestType="wait" />
      </waiter-list>
    </pagelock>
    <pagelock fileid="1" pageid="3017343" dbid="5" objectname="" id="lockd33965a80" mode="S" associatedObjectId="72057594073317376">
      <owner-list>
        <owner id="process569f048" mode="S" />
      </owner-list>
      <waiter-list>
        <waiter id="process8ee3dc8" mode="IX" requestType="wait" />
      </waiter-list>
    </pagelock>
  </resource-list>
</deadlock>

索引详情:

[PK_Table2] PRIMARY KEY CLUSTERED ([Id] ASC);
[IX_Table2]([Column5] ASC, [Status] ASC) INCLUDE ( [Id],[ParentId],[Column3],[Column4]) WHERE ([Column5] IS NULL);

ID 为 72057594073317376 (associatedObjectId) 的对象是:[IX_Table2]

【问题讨论】:

  • 索引提示,真的吗?您正在使用哪些其他“自定义”?表上的索引是如何定义的?
  • 选择从表过滤状态中获取TOP x,而更新正在更新状态...
  • @Shnugo 我明白这一点。但这不应该只是阻止一个交易吗?为什么会出现僵局?
  • 这也类似于dba.stackexchange.com/questions/62947/… - 我认为这是关键,即使SELECT 只使用非聚集索引,它仍然需要锁定聚集索引以防止更新聚类键。关联的 object_id 72057594073317376 是聚集索引还是非聚集索引的 object_id?

标签: sql-server database sql-server-2008-r2 deadlock


【解决方案1】:

只要不使用脏读,选择总是需要锁。

更新查询更改索引。您真的不希望选择查询甚至注意到索引已更改吗?你只是偶尔从你的查询中得到随机的废话(事实上,正是在你这里的场景中——而不是死锁,你会得到格式错误的数据)。

当然,选择通常不会使用排他锁——即使在这种情况下,您也可以看到锁是共享的,而不是排他的。但这仍然意味着任何想要写入该数据的人都不能。而更新语句需要完全做到这一点——同时在索引上持有一个排他锁,这是 select 需要完成的。

一般来说,死锁是无法避免的。它们是您应该准备的预期行为 - 应用程序中的典型响应应该是在收到 1205 错误时重复事务。这是性能和便利性之间的折衷,而不损害正确性。得到错误的进程是随机选择的,所以你在读写时都需要这个。

在您的情况下,很明显您正在更改聚集索引 - 这通常是个坏主意,并且会导致大量死锁机会(毕竟,您正在 重建表,至少部分)。考虑将索引更改为更符合应用程序实际执行的读写操作。如果它经常发生,也可能对性能不利。

编辑:实际上,锁似乎在IX_Table2 的两页上 - 曾经是钥匙的一页,以及更改后需要的一页。这两个锁是按顺序获取的,并且与选择的顺序不同。鉴于索引的布局,这将相对经常发生 - 因为这两个语句都处理为空的Column5。我不认为在这种情况下它真的是可以避免的——也许你可以稍微调整一下索引布局,但这只有在死锁造成实际问题时才有意义——如果你每天只损失几秒钟或更短的时间,那很可能是浪费了努力并可能产生负面影响。

有关分析和解决 MS SQL 死锁的更多信息,请尝试How to resolve a deadlock。如果您需要更多信息来解决问题,请咨询您的 DBA,并考虑在 DBA Stack Exchange 上发布问题 - 确保包含所有必要信息,至少包括所涉及表的 DDL,包括索引。使用 sp_help 将死锁报告中的对象 ID 转换为 DDL 中的实际名称。

【讨论】:

  • 您将聚簇索引与聚簇键混淆了,更改聚簇键通常是不好的,但如果不更新聚簇索引,实际上不可能更新表(不是堆)。此外,Select 语句实际上从未触及 table2 上的聚集索引,因此理论上,update 语句应该更新 table2 上的聚集索引,然后被 select 阻塞,然后当 select 完成时,它应该完成更新非聚集索引。
  • 应用程序频繁读取和修改Table2 的某些行(status 列)。如何避免聚集索引更改? (聚集索引键列只是一个自增的 bigint)。
  • @GarethD 没错!那是我的问题。在这里,SELECT 和 UPDATE 应该阻止一个,但为什么会出现死锁?
  • @mshsayem 请注意,在表具有聚集索引的情况下,所有非聚集索引都将聚集键作为索引的一部分,因此在更新聚集时需要更新键。
  • @mshsayem 添加了更多信息。这是一个典型的、几乎不可避免的死锁——索引更新必须是“原子的”并导致两个页面发生变化,需要两个单独的锁。查询也需要多个锁,因为它需要对行进行排序,有效地跨越整个过滤器范围 - 并且排序不能在查询中间改变。
【解决方案2】:

如果您仔细查看图表,您会看到:

读取器,进程:process569f048 在页面:3017343 上有一个共享锁,并且正在等待对象 72057594073317376 的页面:3017144 上的共享锁

更新进程:process8ee3dc8 在页面 3017144 上有一个 IX 锁,正在等待对象 72057594073317376 的 3017343 上的 IX 锁。

这就是死锁的地方。

要查找提到的对象,您可以使用从stack overflow answer here 收集的以下信息 对象 id 指的是在 sys.partitions 中找到的 hobts(堆或二叉树)。

在数据库 5 中尝试以下查询,您会发现受影响的对象和索引。

SELECT hobt_id, object_name(p.[object_id]), index_id 
FROM sys.partitions p 
WHERE hobt_id = 72057594073317376

正如我在评论中指出的那样,在表具有聚集索引的情况下,所有非聚集索引都将聚集键作为索引的一部分,因此在更新聚集键的情况下需要更新。 我怀疑这个对象会是二级索引,需要被更新更新,可能是因为它是最后一页。

【讨论】:

  • 我没有看到这个答案有任何问题,但想给出 TL;DR 版本。问题中的断言“SELECT 查询需要 IX 锁”是错误的。
  • 谢谢!我运行了您提到的查询,但它没有返回对象 72057594073317376
  • 嗯。从一个新的死锁图中得到了 id,我发现对象是[Table2].[IX_Table2],正如预期的那样。
  • 我认为您的意思是:(可能)两个查询都在访问IX_Table2 的最后几页;他们需要页面锁定,而SELECT 试图锁定一些页面,UPDATE 锁定其他页面,直到每个页面都需要已经被另一个页面锁定的其余页面。
  • @BenThul 谢谢!我现在明白了。我把waiter-list 和owner-list 解释错了。死锁实际上是由于查询持有不兼容的 PAGE 锁而发生的。
【解决方案3】:

这是我对死锁图的错误解释。 wait-resource 字段清楚地表明:

  • SELECT 进程在IX_Table2 的PAGE 上持有S 锁定:3017343
  • UPDATE 进程在IX_Table2 的PAGE 上持有IX 锁: 3017144
  • SELECT 需要带有 S 锁定的页面 3017144;但它由UPDATE 持有
  • UPDATE 需要带有 IX 锁的页面 3017343;但它由SELECT 持有
  • IX 和 S 模式不兼容。所以,死锁。
  • 而且,SELECT 并没有要求 IX 锁定

修复(暂时):

【讨论】:

    猜你喜欢
    • 2014-11-12
    • 2011-09-29
    • 2018-12-07
    • 2013-07-26
    • 1970-01-01
    • 1970-01-01
    • 2014-01-06
    • 2022-07-19
    • 2017-04-01
    相关资源
    最近更新 更多