为什么 select 查询需要 IX 锁?

Why does a select query require an IX lock?

在检查下面的死锁图时,我发现一个 SELECT 查询(仅在第一个进程 process569f048 执行的 SP 内部查询)和一个 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]

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

更新查询更改了索引。您真的希望 select 查询甚至 都不会注意到 索引已更改吗?您偶尔会从查询中得到随机的废话(事实上,正是在您遇到的情况下 - 而不是死锁,您会得到格式错误的数据)。

当然,select 通常不会采用独占锁 - 即使在这种情况下,您也可以看到锁是共享的,而不是独占的。但这仍然意味着任何想要 write 到该数据的人都不能。 update 语句需要做到这一点 - 同时持有索引的独占锁,select 需要完成。

通常无法避免死锁。它们是您应该做好准备的预期行为 - 您的应用程序中的典型响应应该是在您收到 1205 错误时重复该事务。这是性能和便利性之间的折衷,不会危及正确性。出现错误的进程是随机选择的,因此您在读取和写入时都需要它。

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

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

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

如果仔细观察图表,您会发现:

reader,进程:process569f048 在页面:3017343 上有共享锁,正在等待对象 72057594073317376 的页面:3017144 上的共享锁

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

这就是死锁所在。

要查找引用的对象,您可以使用从 stack overflow answer here 中收集的以下信息 对象id指的是hobts(Heap Or Binary Tree),在sys.partitions.

中找到

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

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

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

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

  • SELECT 进程在 IX_Table2PAGE 上持有 S 锁:3017343
  • UPDATE 进程在 IX_Table2PAGE 上持有 IX 锁: 3017144
  • SELECT 需要带 S 锁的页面 3017144;但它由 UPDATE
  • 持有
  • UPDATE 需要带 IX 锁的页面 3017343;但它由 SELECT
  • 持有
  • IXS 模式不兼容。所以,陷入僵局。
  • 而且,SELECT 没有要求 IX

修复(暂时):