一直没有认真了解UPDATE操作的锁,最近在MSDN论坛上看到一个问题,询问堆表更新的死锁问题,问题很简单,有类似这样的表及数据:
CREATE TABLE dbo.tb( c1 int, c2 char(10), c3 varchar(10) ); GO DECLARE @id int; SET @id = 0; WHILE @id <5 BEGIN; SET @id = @id + 1; INSERT dbo.tb VALUES( @id, 'b' + RIGHT(10000 + @id, 4), 'c' + RIGHT(100000 + @id, 4) ); END;
在查询一中执行更新操作:
BEGIN TRAN UPDATE dbo.tb SET c2 = 'xx' WHERE c1 = 2; WAITFOR DELAY '00:00:30'; UPDATE dbo.tb SET c2 = 'xx' WHERE c1 = 5; ROLLBACK;
在查询一执行开始后,马上在查询二中执行下面的操作
BEGIN TRAN UPDATE dbo.tb SET c2 = 'xx' WHERE c1 = 1; ROLLBACK;
为什么会出现死锁,如果条件改为 c1 = 4 则不会死锁。
开始的时候想得比较简单,死锁的表现是形成循环等待(对于两个查询而言,可以简单地认为就是在相互等待对方锁定资源的释放)。
对于这个例子而言,第一个查询更新两次,会先更新并锁定一条记录,然后等待第二个更新;但第二个查询只会更新一条记录,它要么与第一个查询冲突,无法获得锁,需要等待查询一完成,这个时候它并没有锁定什么;要么能够获得锁,完成更新。似乎不应该会出现死锁,死锁会不会是其他原因导致。
在自己的电脑上简单测试了一下,似乎也确实没有死锁。
但后面通过Profile跟踪更新操作的下锁情况才发现,自己的分析大错特错了。主要原因在于没有正确理解更新操作是如何用锁的。
在联机帮助上“锁模式”中有关于更新的U(更新锁)和X(排它锁)的说明
http://msdn.microsoft.com/zh-cn/library/ms175519(v=sql.105).aspx
不过说得确实挺模糊的,里面还提到了S锁,我一直以为是查询数据过程中用的S锁(也 SELECT 一样),找到满足条件的记录后用U锁,再转换为X锁做更新。
Profile(事件探查器)跟踪的结果让我知道了这是一个错误的理解,在Profile中新建一个跟踪,选择Locks中的Lock:Acquired(加锁),Lock:Acquired(释放锁)解两个事件,在筛选中设置只跟踪测试用的查询窗口对应的spid(可以执行 PRINT @@SPID获得),然后执行一个更新语句,比如 UPDATE dbo.tb SET c2 = 'xx' WHERE c1 = 3
在Profile中可以看到,对于每条记录都有加 U 锁的操作,对于不满足条件的记录,会马上释放U锁;对于满足条件的记录,最终转换为X锁。如下图所示。
注意一下,在这个跟踪结果里面,并没有出现S锁。
另外学做了一些测试:
通过加大记录量做更新测试,会发现数据扫描涉及的记录都有U锁,并不限于更新记录所在的页。这从另一个角度说明了大表中Scan 可怕。
当使用索引Scan的时候,也会通过跟踪发现所Scan的索引资源有U锁,如果更新不涉及索引变化,