更新锁(U 锁)可以防止常见的死锁。在可重复读或可序列化事务中,此事务读取数据 [获取资源(页或行)的共享锁(S 锁)],然后修改数据 [此操作要求锁转换为排他锁(X 锁)]。如果两个事务获得了资源上的共享模式锁,然后试图同时更新数据,则一个事务尝试将锁转换为排他锁(X 锁)。共享模式到排他锁的转换必 须 等待一段时间,因为一个事务的排他锁与其他事务的共享模式锁不兼容;发生锁等待。第二个事务试图获取排他锁(X 锁)以进行更新。由于两个事务都要转换为排他锁(X 锁),并且每个事务都等待另一个事务释放共享模式锁,因此发生死锁。
若要避免这种潜在的死锁问题,请使用更新锁(U 锁)。一次只有一个事务可以获得资源的更新锁(U 锁)。如果事务修改资源,则更新锁(U 锁)转换为排他锁(X 锁)。
对于MSDN中举的这个例子,困惑了几天。因为我一直想不明白,A事务就算在资源上有了更新锁,在更新锁转为排他锁的时候,事务B在这个资源上不是还有共享锁么?那么A事务的排他锁怎么会转成功呢?显然排他锁和共享锁是互斥的。那么还是会有死锁发生啊?怎么能够避免呢?
找了一天的资料,经过自己试验,才发现原来是MSDN“语焉不详”。更新锁只有在事务隔离级别是Read Committed(提交读)或者它之下的时候,更新锁才能在这种情况(MSDN中的例子)下避免死锁,如果隔离级别在Read Committed(提交读)之上,比如Repeatable Read(可重复读),就算是MSDN中的那个例子,也一样会导致死锁。试验SQL如下(AdventureWorks数据库,两个级别分别开两个查询窗口,执行同样的sql,如果你的手速不够快,请把延时设高一点):
Read Committed(提交读)级别:
use adventureworks
go
set transaction isolation level Read Committed
begin tran
select * from DatabaseLog where DatabaseLogID = 1
waitfor delay '00:00:05'
update DatabaseLog set PostTime = '2006-04-26 11:48:51.160' where DatabaseLogID = 1
commit tran
Repeatable Read(可重复读)级别:
use adventureworks
go
set transaction isolation level repeatable read
begin tran
select * from DatabaseLog where DatabaseLogID = 1
waitfor delay '00:00:05'
update DatabaseLog set PostTime = '2006-04-26 11:48:51.160' where DatabaseLogID = 1
commit tran
在提交读隔离级别,这段SQL执行同时执行,并不会产生死锁。因为在这个隔离级别,共享锁是用完就释放的。并不会等到事务结束才释放。在可重复读以上级别,共享锁会持续到事务结束,因此,就算是用更新锁中继更新数据,也会产生死锁,因为在更新锁转为排他锁的时候,还有另外一个事务的共享锁没有释放,报出错误1205(死锁)