Boo*_*ood 13 index sql-server locking
我正在解决一个死锁问题,同时我注意到当我在 id 字段上使用聚集和非聚集索引时,锁定行为是不同的。如果将聚集索引或主键应用于 id 字段,则死锁问题似乎已解决。
我有不同的事务对不同的行进行一次或多次更新,例如事务 A 只会更新 ID=a 的行,tx 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
)
Run Code Online (Sandbox Code Playgroud)
死锁跟踪
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
Run Code Online (Sandbox Code Playgroud)
另外一个有趣且可能的相关发现是聚集索引和非聚集索引似乎具有不同的锁定行为
使用聚集索引时,键上有排他锁,更新时RID上有排他锁,这是意料之中的;如果使用非聚集索引,则在两个不同的 RID 上有两个排他锁,这让我感到困惑。
如果有人也能解释为什么会有所帮助。
测试 SQL:
use SQL2012_707688_webows;
begin transaction;
update [user] with (rowlock) set todoOrder='{1}' where id = 63501
exec sp_lock;
commit;
Run Code Online (Sandbox Code Playgroud)
使用 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
Run Code Online (Sandbox Code Playgroud)
使用 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
Run Code Online (Sandbox Code Playgroud)
EDIT1:没有任何索引的死锁细节
假设我有两个 tx A 和 B,每个都有两个更新语句,当然是不同的行
tx A
update [user] with (rowlock) set todoOrder='{1}' where id = 63501
update [user] with (rowlock) set todoOrder='{2}' where id = 63501
Run Code Online (Sandbox Code Playgroud)
乙
update [user] with (rowlock) set todoOrder='{3}' where id = 63502
update [user] with (rowlock) set todoOrder='{4}' where id = 63502
Run Code Online (Sandbox Code Playgroud)
{1} 和 {4} 可能会出现死锁,因为
在 {1} 处,第 63502 行需要 U 锁,因为它需要进行表扫描,而 X 锁可能已保留在第 63501 行上,因为它符合条件
在 {4} 行,63501 行请求 U 锁,63502 行 X 锁已持有
所以我们有 txA 持有 63501 并等待 63502 而 txB 持有 63502 等待 63501,这是一个死锁
EDIT2 :原来我的测试用例的一个错误在这里造成了不同的情况 很抱歉混淆,但该错误造成了不同的情况,并且似乎最终导致了死锁。
由于保罗的分析在这种情况下确实帮助了我,因此我将其作为答案接受。
由于我的测试用例的bug,两个事务txA和txB可以更新同一行,如下:
发送
update [user] with (rowlock) set todoOrder='{1}' where id = 63501
update [user] with (rowlock) set todoOrder='{2}' where id = 63501
Run Code Online (Sandbox Code Playgroud)
乙
update [user] with (rowlock) set todoOrder='{3}' where id = 63501
Run Code Online (Sandbox Code Playgroud)
{2} 和 {3} 在以下情况下可能会出现死锁:
txA 在 RID 上请求 U 锁同时在 RID 上持有 X 锁(由于 {1} 的更新) txB 在 RID 上请求 U 锁同时在密钥上保持 U 锁
Pau*_*ite 16
...为什么使用聚集索引,死锁仍然存在(虽然命中率似乎下降了)
这个问题不是很清楚(例如id
,每个事务中有多少更新和哪些值),但是一个明显的死锁场景出现在单个事务中的多个单行更新中,其中存在重叠的[id]
值,并且 id 是以不同的[id]
顺序更新:
[T1]: Update id 2; Update id 1;
[T2]: Update id 1; Update id 2;
Run Code Online (Sandbox Code Playgroud)
死锁序列: T1 (u2), T2 (u1), T1 (u1) wait , T2 (u2) wait。
通过在每个事务中严格按照 id 顺序更新(在同一路径上以相同顺序获取锁),可以避免这种死锁序列。
使用聚集索引时,键上有排他锁,更新时RID上有排他锁,这是意料之中的;如果使用非聚集索引,则在两个不同的 RID 上有两个排他锁,这让我感到困惑。
通过在 上的唯一聚集索引id
,对聚集键进行排他锁以保护对行内数据的写入。需要单独的RID
排他锁来保护对 LOBtext
列的写入,默认情况下该列存储在单独的数据页上。
当表是一个只有一个非聚集索引的堆时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
Run Code Online (Sandbox Code Playgroud)
只有一个非聚集索引,无法应用相同的优化,因为我们需要从一个 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
Run Code Online (Sandbox Code Playgroud)
请注意,LOB 数据是在表更新迭代器中读取和写入的。更复杂的计划和多个读写路径会增加死锁的机会。
最后,我不禁注意到表定义中使用的数据类型。您不应text
在新工作中使用已弃用的数据类型;另一种选择是,如果您确实需要在此列中存储最多 2GB 的数据,则是varchar(max)
. text
和之间的一个重要区别varchar(max)
是text
数据默认存储在行外,而默认varchar(max)
存储在行内。
仅当您需要这种灵活性时才使用 Unicode 类型(例如,很难看出为什么 IP 地址需要 Unicode)。此外,为您的属性选择适当的长度限制 - 255 似乎不太可能正确。
补充阅读:
死锁和活锁常见模式
Bart Duncan 的死锁故障排除系列
跟踪锁可以通过多种方式完成。带有高级服务的 SQL Server Express(仅限 2014 和 2012 SP1 以后)包含Profiler工具,这是一种查看锁获取和释放详细信息的受支持方式。
归档时间: |
|
查看次数: |
27055 次 |
最近记录: |