SQL Server 2005:在读取提交事务隔离级别下的键范围锁?

3

我正在协助解决一个使用SQL Server 2005的.NET应用程序中的死锁问题。下面是跟踪记录中的XML数据。

真正困扰我的是,当事务隔离级别为读取提交时,PK_Exp_Experience_PriorFirm上的RangeX-X锁。

我所阅读的所有内容都表明,只有在使用事务隔离级别“串行化”时,您才会获得键范围锁定。到目前为止,我找不到我们的应用程序中设置隔离级别为读取提交以外任何级别的地方,下面的XML也表明我们正在使用读取提交。

但是,如果我们正在使用读取提交,我不明白跟踪记录如何显示存在键范围锁。是否有人有关于可能发生这种情况的想法?

<deadlock-list>
  <deadlock victim="processc2f438">
    <process-list>
      <process id="processc2f438" taskpriority="0" logused="13488" waitresource="KEY: 120:72057594583646208 (8201498b6efe)" waittime="484" ownerId="693258089" transactionname="user_transaction" lasttranstarted="2009-01-06T16:33:27.817" XDES="0xa71ce370" lockMode="U" schedulerid="1" kpid="9112" status="suspended" spid="53" sbid="0" ecid="0" priority="0" transcount="2" lastbatchstarted="2009-01-06T16:33:27.863" lastbatchcompleted="2009-01-06T16:33:27.863" clientapp=".Net SqlClient Data Provider" hostname="CHQAPT3" hostpid="6464" loginname="AppUser" isolationlevel="read committed (2)" xactid="693258089" currentdb="120" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
        <executionStack>
          <frame procname="adhoc" line="1" stmtstart="108" sqlhandle="0x0200000015d9962978fc6206b09e4c872150511b455e8923">
            UPDATE Exp_Experience_PriorFirm SET RelatedGuid = @newGuid WHERE RelatedGuid = @oldGuid
          </frame>
          <frame procname="mssqlsystemresource.sys.sp_executesql" line="1" sqlhandle="0x0400ff7fbe80662601000000000000000000000000000000">
            sp_executesql
          </frame>
          <frame procname="MyDb.dbo.Contact_MergeRelationships" line="74" stmtstart="4754" stmtend="4976" sqlhandle="0x0300780036a608461ed8af00669b00000100000000000000">
            EXEC sp_executesql @sql,
            N'@oldGuid uniqueidentifier, @newGuid uniqueidentifier',
            @oldGuid, @newGuid
          </frame>
          <frame procname="MyDb.dbo.Contact_Company_MergeRelationships" line="8" stmtstart="312" sqlhandle="0x03007800b271a129c8ccaf00669b00000100000000000000">
            EXEC Contact_MergeRelationships @oldGuid, @newGuid, 'Contact_Company', @excludedTableNames
          </frame>
        </executionStack>
        <inputbuf>
          Proc [Database Id = 120 Object Id = 698446258]
        </inputbuf>
      </process>
      <process id="processeb5d68" taskpriority="0" logused="14212" waitresource="KEY: 120:72057594594066432 (7c02a3a5890e)" waittime="2312" ownerId="693243114" transactionname="user_transaction" lasttranstarted="2009-01-06T16:33:20.957" XDES="0x8cdb9450" lockMode="S" schedulerid="2" kpid="9000" status="suspended" spid="73" sbid="0" ecid="0" priority="0" transcount="2" lastbatchstarted="2009-01-06T16:33:29.770" lastbatchcompleted="2009-01-06T16:33:29.770" clientapp=".Net SqlClient Data Provider" hostname="CHQAPT3" hostpid="6464" loginname="AppUser" isolationlevel="read committed (2)" xactid="693243114" currentdb="120" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
        <executionStack>
          <frame procname="MyDb.dbo.Contact_Company_Delete" line="27" stmtstart="1128" sqlhandle="0x03007800b0e5761877cbaf00669b00000100000000000000">
            DELETE FROM Contact WHERE GUID = @Guid;
          </frame>
        </executionStack>
        <inputbuf>
          Proc [Database Id = 120 Object Id = 410445232]
        </inputbuf>
      </process>
    </process-list>
    <resource-list>
      <keylock hobtid="72057594583646208" dbid="120" objectname="MyDb.dbo.Exp_Experience_PriorFirm" indexname="PK_Exp_Experience_PriorFirm" id="lockd1d43f80" mode="RangeX-X" associatedObjectId="72057594583646208">
        <owner-list>
          <owner id="processeb5d68" mode="RangeX-X"/>
        </owner-list>
        <waiter-list>
          <waiter id="processc2f438" mode="U" requestType="wait"/>
        </waiter-list>
      </keylock>
      <keylock hobtid="72057594594066432" dbid="120" objectname="MyDb.dbo.Contact_PersonCompanyLocation" indexname="PK_Contact_PersonCompanyLocation" id="lockd20c4380" mode="X" associatedObjectId="72057594594066432">
        <owner-list>
          <owner id="processc2f438" mode="X"/>
        </owner-list>
        <waiter-list>
          <waiter id="processeb5d68" mode="S" requestType="wait"/>
        </waiter-list>
      </keylock>
    </resource-list>
  </deadlock>
</deadlock-list>
4个回答

9

  1. 隔离级别可能会因为代码自动升级或意外引入到比可重复读更高的级别而成为问题的原因。
- Ethan Wang

4
您正在使用预期的READ COMMITTED隔离级别。
如果UPDATE获取了聚集索引上的排它键锁并修改了一行,那么这样的死锁可能会发生,并且该锁会阻止SELECT在聚集索引上进行书签查找。
通过创建一个覆盖非聚集索引,可以经常消除此类锁。
您可以为数据库设置READ_COMMITTED_SNAPSHOT ON。这将更改SELECT语句读取提交的数据的方式;它们不会获取共享锁,而是读取在SELECT语句开始时启动的事务更改的任何数据的先前版本(快照)。但是这并非完全免费;代价是tempDB中的活动增加。【READ COMMITTED SNAPSHOT模式下也存在触发器问题的潜在风险。】

我同意这些都是死锁的可能解释和好建议。但是,如果隔离级别是读提交,我仍然不明白为什么会发生RangeX-X锁定。我想了解为什么我们在一系列键上有一个排他锁,而不是单个键。 - Amy T

2

另一个常见的解释可能与启用了IGNORE_DUP_KEY选项的UNIQUE索引有关。

BOL中指出,此选项指定插入操作尝试将重复键值插入到唯一索引中时的错误响应。IGNORE_DUP_KEY选项仅适用于在创建或重建索引之后进行的插入操作。执行CREATE INDEX、ALTER INDEX或UPDATE时该选项无效。默认值为OFF。

ON 当重复键值被插入唯一索引时,将发生警告消息。只有违反唯一性约束的行将失败。

OFF 当重复键值被插入唯一索引时,将发生错误消息。整个INSERT操作将被回滚。

这里未提及的是,在启用此选项时,插入期间会强制执行SERIALIZABLE隔离。个人认为没有掌握这样做的固有要求,因为某些插入的行可能会被丢弃,但事实就是如此。请SQL开发团队在这里发表意见...

可以轻松演示这种行为;

首先创建一个具有典型PK的新表:

CREATE TABLE [dbo].[Test_RC_TIL_RangeLocks](
    [RID] [int] IDENTITY(1,1) NOT NULL,
    [Col1] [int] NOT NULL,
    [Col2] [int] NOT NULL,
    [Col3] [int] NOT NULL
) ON [PRIMARY]

接下来,我们想在 Col1 和 Col2 上添加一个 UNIQUE 索引,并将 IGNORE_DUP_KEY 设置为 ON:

CREATE UNIQUE NONCLUSTERED INDEX [UIX_Test_RC_TIL_RangeLocks] ON [dbo].[Test_RC_TIL_RangeLocks](
    [Col1] ASC,
    [Col2] ASC
)WITH (
    PAD_INDEX  = OFF, 
    STATISTICS_NORECOMPUTE  = OFF, 
    SORT_IN_TEMPDB = OFF, 
    IGNORE_DUP_KEY = ON, --<<**THE OFFENDER**>>
    DROP_EXISTING = OFF, 
    ONLINE = OFF, 
    ALLOW_ROW_LOCKS  = ON, 
    ALLOW_PAGE_LOCKS  = ON) 
ON [PRIMARY]

接下来我们将添加5行,看看会发生什么...

SET TRANSACTION ISOLATION LEVEL READ COMMITTED
DECLARE @C int
SELECT @C=5
BEGIN TRANSACTION
WHILE @C>0
BEGIN
    INSERT Test_RC_TIL_RangeLocks(Col1,Col2,Col3)
    VALUES (@C,@C+1,2*@C + 100)
    SET @C=@C-1
END
SELECT * FROM Test_RC_TIL_RangeLocks
EXEC sp_lock @@SPID
COMMIT

正如预期的那样,我们已经添加了五行:

RID Col1    Col2    Col3
1   5       6       110
2   4       5       108
3   3       4       106
4   2       3       104
5   1       2       102

我们感兴趣的锁是:

sid ObjId     IndId Type Resource                  Mode
53  0             0 DB                             S
53  0             0 DB                             S
53  402100473     1 KEY (8194443284a0)             X
53  402100473     2 KEY (550e0a2a4b96)             RangeX-X
53  402100473     2 KEY (ffffffffffff)             RangeS-U
53  1131151075    0 TAB                            IS
53  402100473     1 PAG 1:744                      IX
53  402100473     2 PAG 1:748                      IX
53  402100473     1 KEY (98ec012aa510)             X
53  402100473     1 KEY (a0c936a3c965)             X
53  402100473     2 KEY (ec04ac4bee1f)             RangeX-X
53  402100473     0 TAB                            IX
53  402100473     2 KEY (0207a0a08e23)             RangeX-X
53  402100473     2 KEY (7112ec63c430)             RangeX-X
53  402100473     1 KEY (59855d342c69)             X
53  402100473     1 KEY (61a06abd401c)             X
53  338100245     0 TAB                            IX

啊,可怕的SERIALIZABLE Key Range Locks在一个简单的插入操作中被READ COMMITTED隔离级别激活了!
当BOL说: 在键范围锁定之前,必须满足以下条件: • 事务隔离级别必须设置为SERIALIZABLE。
请记住,这并不总是正确的...
附注:作为设计说明,盲目地消除可能具有唯一行值的重复键通常是不好的做法。更好的做法是确保您不会尝试将重复键作为INSERT语句的一部分插入...
干杯...

0
根据SQL文档,你的事务(或其他事务)以串行化级别运行。
这里是有关键范围锁的文档here。第一句话如下:
“在使用可串行化事务隔离级别时,键范围锁会隐式地保护由Transact-SQL语句读取的记录集中包含的一系列行。”

请提供相关文档的链接。不确定这是否作为答案添加了任何相关价值。 - Champ
根据要求添加了到TechNet文档的链接。我不确定这是否是运维问题。但是,在Serializable隔离级别下经常会导致键范围锁,因此值得一提并进行调查。 - Charles Bretana

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接