使用非聚集索引更新不同行时出现死锁


13

我正在解决一个死锁问题,但我发现在id字段上使用聚集索引和非聚集索引时,锁的行为有所不同。如果将聚集索引或主键应用于id字段,则似乎可以解决死锁问题。

我有不同的事务对一个不同的行进行一个或多个更新,例如事务A将仅更新ID = a的行,事务B将仅触摸ID = b的行,等等。

而且我知道没有索引,更新将获取所有行的更新锁,并在必要时隐式转换为排他锁,这最终将导致死锁。但是我无法找出为什么非聚集索引仍然存在死锁(尽管命中率似乎下降了)

数据表:

CREATE TABLE [dbo].[user](
    [id] [int] IDENTITY(1,1) NOT NULL,
    [userName] [nvarchar](255) NULL,
    [name] [nvarchar](255) NULL,
    [phone] [nvarchar](255) NULL,
    [password] [nvarchar](255) NULL,
    [ip] [nvarchar](30) NULL,
    [email] [nvarchar](255) NULL,
    [pubDate] [datetime] NULL,
    [todoOrder] [text] NULL
)

死锁跟踪

deadlock-list
deadlock victim=process4152ca8
process-list
process id=process4152ca8 taskpriority=0 logused=0 waitresource=RID: 5:1:388:29 waittime=3308 ownerId=252354 transactionname=user_transaction lasttranstarted=2014-04-11T00:15:30.947 XDES=0xb0bf180 lockMode=U schedulerid=3 kpid=11392 status=suspended spid=57 sbid=0 ecid=0 priority=0 trancount=2 lastbatchstarted=2014-04-11T00:15:30.953 lastbatchcompleted=2014-04-11T00:15:30.950 lastattention=1900-01-01T00:00:00.950 clientapp=.Net SqlClient Data Provider hostname=BOOD-PC hostpid=9272 loginname=getodo_sql isolationlevel=read committed (2) xactid=252354 currentdb=5 lockTimeout=4294967295 clientoption1=671088672 clientoption2=128056
executionStack
frame procname=adhoc line=1 stmtstart=62 sqlhandle=0x0200000062f45209ccf17a0e76c2389eb409d7d970b0f89e00000000000000000000000000000000
update [user] WITH (ROWLOCK) set [todoOrder]=@para0 where id=@owner
frame procname=unknown line=1 sqlhandle=0x00000000000000000000000000000000000000000000000000000000000000000000000000000000
unknown
inputbuf
(@para0 nvarchar(2)<c/>@owner int)update [user] WITH (ROWLOCK) set [todoOrder]=@para0 where id=@owner
process id=process4153468 taskpriority=0 logused=4652 waitresource=KEY: 5:72057594042187776 (3fc56173665b) waittime=3303 ownerId=252344 transactionname=user_transaction lasttranstarted=2014-04-11T00:15:30.920 XDES=0x4184b78 lockMode=U schedulerid=3 kpid=7272 status=suspended spid=58 sbid=0 ecid=0 priority=0 trancount=2 lastbatchstarted=2014-04-11T00:15:30.960 lastbatchcompleted=2014-04-11T00:15:30.960 lastattention=1900-01-01T00:00:00.960 clientapp=.Net SqlClient Data Provider hostname=BOOD-PC hostpid=9272 loginname=getodo_sql isolationlevel=read committed (2) xactid=252344 currentdb=5 lockTimeout=4294967295 clientoption1=671088672 clientoption2=128056
executionStack
frame procname=adhoc line=1 stmtstart=60 sqlhandle=0x02000000d4616f250747930a4cd34716b610a8113cb92fbc00000000000000000000000000000000
update [user] WITH (ROWLOCK) set [todoOrder]=@para0 where id=@uid
frame procname=unknown line=1 sqlhandle=0x00000000000000000000000000000000000000000000000000000000000000000000000000000000
unknown
inputbuf
(@para0 nvarchar(61)<c/>@uid int)update [user] WITH (ROWLOCK) set [todoOrder]=@para0 where id=@uid
resource-list
ridlock fileid=1 pageid=388 dbid=5 objectname=SQL2012_707688_webows.dbo.user id=lock3f7af780 mode=X associatedObjectId=72057594042122240
owner-list
owner id=process4153468 mode=X
waiter-list
waiter id=process4152ca8 mode=U requestType=wait
keylock hobtid=72057594042187776 dbid=5 objectname=SQL2012_707688_webows.dbo.user indexname=10 id=lock3f7ad700 mode=U associatedObjectId=72057594042187776
owner-list
owner id=process4152ca8 mode=U
waiter-list
waiter id=process4153468 mode=U requestType=wait

还有一个有趣且可能相关的发现是,聚集索引和非聚集索引似乎具有不同的锁定行为

使用聚簇索引时,键上有排他锁,更新时RID上有排他锁,这是可以预期的。如果使用非聚集索引,则在两个不同的RID上有两个互斥锁,这使我感到困惑。

如果有人也可以解释为什么会有所帮助。

测试SQL:

use SQL2012_707688_webows;
begin transaction;
update [user] with (rowlock) set todoOrder='{1}' where id = 63501
exec sp_lock;
commit;

以id作为聚集索引:

spid    dbid    ObjId   IndId   Type    Resource    Mode    Status
53  5   917578307   1   KEY (b1a92fe5eed4)                      X   GRANT
53  5   917578307   1   PAG 1:879                               IX  GRANT
53  5   917578307   1   PAG 1:1928                              IX  GRANT
53  5   917578307   1   RID 1:879:7                             X   GRANT

id为非聚集索引

spid    dbid    ObjId   IndId   Type    Resource    Mode    Status
53  5   917578307   0   PAG 1:879                               IX  GRANT
53  5   917578307   0   PAG 1:1928                              IX  GRANT
53  5   917578307   0   RID 1:879:7                             X   GRANT
53  5   917578307   0   RID 1:1928:18                           X   GRANT

EDIT1:没有任何索引的死锁详细信息
说我有两个tx A和B,每个都有两个update语句,当然是
tx A的 不同行

update [user] with (rowlock) set todoOrder='{1}' where id = 63501
update [user] with (rowlock) set todoOrder='{2}' where id = 63501

TX B

update [user] with (rowlock) set todoOrder='{3}' where id = 63502
update [user] with (rowlock) set todoOrder='{4}' where id = 63502

{1}和{4}将有可能陷入僵局,因为

在{1}处,由于需要进行表扫描,因此请求对行63502进行U锁定,并且由于X锁与条件匹配,因此本来可以在行63501上保持X锁

在{4}处,请求对行63501进行U锁定,并且对63502行已保留X锁定

所以我们有txA持有63501并等待63502,而txB持有63502等待63501,这是一个死锁

EDIT2:原来我的测试用例的一个错误在这里有所不同 抱歉让您感到困惑,但是该错误却有所不同,并且似乎最终导致了死锁。

由于Paul的分析在这种情况下确实帮助了我,因此我将其作为答案。

由于我的测试用例的错误,两个事务txA和txB可以更新同一行,如下所示:

发射A

update [user] with (rowlock) set todoOrder='{1}' where id = 63501
update [user] with (rowlock) set todoOrder='{2}' where id = 63501

TX B

update [user] with (rowlock) set todoOrder='{3}' where id = 63501

在以下情况下,{2}和{3}可能会出现死锁:

txA在RID上保持X锁的同时请求对U锁(由于更新{1})txB在RID上保持U锁的同时对RID进行U锁


1
我不明白为什么一个事务需要两次更新同一行。
ypercubeᵀᴹ

@ypercube好点,这是我应该改进的地方。但是在这种情况下,我只是想对锁定行为有更好的了解
Bood 2014年

@ypercube经过更多思考之后,我认为具有复杂逻辑的应用程序可能需要在同一TX中两次更新同一行,例如可以是不同的列
Bood 2014年

Answers:


16

...为什么使用聚集索引,死锁仍然存在(尽管命中率似乎下降了)

问题尚不十分清楚(例如id,每个事务中有多少个更新以及对哪个值进行更新),但是一个明显的死锁情况是在单个事务中出现多个单行更新,其中[id]值存在重叠,并且id是以其他[id]顺序更新:

[T1]: Update id 2; Update id 1;
[T2]: Update id 1; Update id 2;

死锁序列: T1(u2),T2(u1),T1(u1)等待,T2(u2)等待

可以通过在每个事务中严格按照id顺序更新(在同一路径上以相同顺序获取锁)来避免此死锁序列。

使用聚簇索引时,键上有排他锁,更新时RID上有排他锁,这是可以预期的。如果使用非聚集索引,则在两个不同的RID上有两个互斥锁,这使我感到困惑。

启用了唯一的聚集索引后id,将对聚集键进行排他锁,以保护对行内数据的写入。需要单独的RID排他锁来保护对LOB text列的写入,默认情况下,该LOB 列存储在单独的数据页上。

当表是仅具有非聚集索引的堆时id,会发生两件事。首先,一个RID排他锁与堆行数据有关,另一个与以前一样是对LOB数据的锁。第二个效果是需要更复杂的执行计划。

使用聚簇索引和简单的单值相等谓词更新,查询处理器可以应用优化,使用单个路径在单个运算符中执行更新(读和写):

单操作员更新

该行是在单个查找操作中定位和更新的,仅需要排他锁(不需要更新锁)。使用示例表的示例锁定序列:

acquiring IX lock on OBJECT: 6:992930809:0 -- TABLE
acquiring IX lock on PAGE: 6:1:59104 -- INROW
acquiring X lock on KEY: 6:72057594233618432 (61a06abd401c) -- INROW
acquiring IX lock on PAGE: 6:1:59091 -- LOB
acquiring X lock on RID: 6:1:59091:1 -- LOB

releasing lock reference on PAGE: 6:1:59091 -- LOB
releasing lock reference on RID: 6:1:59091:1 -- LOB
releasing lock reference on KEY: 6:72057594233618432 (61a06abd401c) -- INROW
releasing lock reference on PAGE: 6:1:59104 -- INROW

仅使用非聚集索引时,无法应用相同的优化,因为我们需要从一个b树结构读取并写入另一个b树结构。多路径计划具有单独的读取和写入阶段:

多迭代器更新

读取时获取更新锁,如果行合格,则转换为排他锁。具有指定模式的示例锁定序列:

acquiring IX lock on OBJECT: 6:992930809:0 -- TABLE
acquiring IU lock on PAGE: 6:1:59105 -- NC INDEX
acquiring U lock on KEY: 6:72057594233749504 (61a06abd401c) -- NC INDEX
acquiring IU lock on PAGE: 6:1:59104 -- HEAP
acquiring U lock on RID: 6:1:59104:1 -- HEAP
acquiring IX lock on PAGE: 6:1:59104 -- HEAP convert to X
acquiring X lock on RID: 6:1:59104:1 -- HEAP convert to X
acquiring IU lock on PAGE: 6:1:59091 -- LOB
acquiring U lock on RID: 6:1:59091:1 -- LOB

releasing lock reference on PAGE: 6:1:59091 
releasing lock reference on RID: 6:1:59091:1
releasing lock reference on RID: 6:1:59104:1
releasing lock reference on PAGE: 6:1:59104 
releasing lock on KEY: 6:72057594233749504 (61a06abd401c)
releasing lock on PAGE: 6:1:59105 

注意,在表更新迭代器中读取和写入了LOB数据。更复杂的计划和多个读写路径增加了出现死锁的机会。

最后,我不禁注意到表定义中使用的数据类型。您不应该将不赞成使用的text数据类型用于新工作。如果您确实需要在此列中存储2GB数据的能力,则选择varchar(max)text和之间的一个重要区别varchar(max)是,text数据默认情况下是行外存储的,而默认情况下是行内varchar(max)存储的。

仅在需要灵活性时才使用Unicode类型(例如,很难理解为什么IP地址需要Unicode)。另外,为属性选择合适的长度限制-255似乎不太可能是正确的。

附加阅读:
死锁和活锁常见模式
Bart Duncan的死锁疑难解答系列

跟踪锁定可以通过多种方式完成。具有高级服务的SQL Server Express仅适用于2014和2012 SP1及更高版本)包含Profiler工具,该工具是查看锁获取和释放详细信息的受支持方式。


极好的答案。如何输出具有“获取...锁定”和“释放锁定参考”消息的日志/跟踪?
Sanjiv Jivan
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.