SQL锁机制高级篇2 下载本文

使用SQL Server 6年多了,在下自认为对SQL Server还是比较熟悉的,而且我喜欢将SQL Server内部的一些 东西搞清楚。

当我在教一门SQL Server编程课程时,我注意到微软MSDN中提到了锁兼容性,在MSDN 列举了一个兼容性关系的表格。

看过这张关系表格,我就想知道是否存在用于更新的意向锁(Intent Update lock)?于是我开始阅读相关的资料。 这篇文章也是我研究的结果。这篇文章的适用读者是那些对隔离级别(isolation level),意向锁,死锁和锁粒度有所了解的。 如果你对这些领域还不了解,那么我建议你在读这篇文章前,应该先去了解和阅读相关资料。

希望这篇文章能够加深你对SQL Server锁的理解,也许有些技巧还能够在SQL Server编程中带来帮助。

必须指出,即使不知道锁是如何工作的,你也能长时间愉快地使用SQL Server,并且能创建高质量的代码和数据库设计。 不过如果你象我那样喜欢探究事情的内部机理,或者你的工作需要你掌握一些性能方面的知识,我很乐意能教你一些有用的东西。

更新锁(Update Locks)

死锁的典型情况是SPID X锁住了资源A,并在等待对资源B进行加锁,而SPID Y锁住了资源B,在等待对资源A加锁,如此就 形成了死锁。如果不理解,查询 MSDN 或者相关的资料。

现在来假想更多情形下的死锁。假设:SPID X在资源A上加了共享锁,SPID Y也在资源A上加了共享锁,因为是共享锁, 所以这样没有问题。现在X想把共享锁升级为排它锁(exclusive lock)以用于更新资源。X就必须等Y释放共享锁才能办到, 当X在等待时,Y也想做同样的事情。这样,X在等Y释放,Y同时在等待X释放,死锁产生了。这种死锁被称为 转换死锁(conversion deadlock)。

这种情况会很常见,为避免这种死锁,就引入了更新锁机制。更新锁允许连接读取资源,同时宣告它因为要编辑数据而要开始 锁住资源了。SQL Server并无法提前知道一个事务要把共享锁转换成排它锁了,当然有一个情况特殊,即只在一个SQL语句中 完成读取然后更新的操作,比如说UPDATE XXX (SELECT YYY ....)这种类型。对于一般的SELECT语句,我们必须显示地 使用UPDLOCK提示。 下面是代码示例:

USE Northwind GO SET TRANSACTION ISOLATION LEVEL REPEATABLE READ GO BEGIN TRAN SELECT * FROM Orders (UPDLOCK) WHERE OrderID = 10633

注意到我打开了事务,但并没有关闭事务。这样锁就始终存在。 如果另外一个连接视图在相同的记录上获取更新锁,就只有等待第一个事务结束后才行。这样就可以演示,在相同资源上, 两个更新锁不相容的效果。

运行SP_LOCK,会显示和上面的操作相关的记录行,字段以及锁的情况:

如我们预想那样,主键OrderID被更新锁锁住了。图中Resource列里面那个

(89003da47679)的值,表示的是 主键10633的哈希值。SQL Server使用哈希表的方式来存储锁信息。

包含那行的记录行,如我们所期望的那样,被更新意向锁锁住了。在resource那列的数值(1:242)表示该数据页面是 数据库的第1个文件,页面编号是#242。而意外的是,SQL Server添加了一个IX的表锁。由于SQL Server不会在 表锁上使用U/IU类型锁,所以在表锁级别上,只能看到X/IX类型锁。

当更新操作中带有where语法,SQL Server会扫描整个表,并且/或者扫描索引,以决定那些记录会被改变。 在从表/索引读取信息之前,SQL Server首先把对象锁住。既然SQL Server知道你提交的是更新事务,那么它 就会选择更新锁,而不是共享锁。这样做就是为了避免前面所提到的死锁情况--转换死锁(conversion deadlock)。

当SQL Server确定那些记录行需要改变后,在这些记录上,它会把更新锁进一步升级为排它锁,如果是堆表(heap table),那么锁加 在RID(行标识符)上,如果是聚集索引表,锁加在主键上。这就意味着更新锁会立刻升级为排它锁,因此当你执行UPDATE 操作时,几乎不可能看到这个更新过程。

不过,也有例外。如果SQL Server使用一个索引来定位记录行,它就会锁住索引页,在索引上加的就是更新锁。 如果不改变任何包含在该索引中的数据列,更新锁不会升级为排它锁。下面是一个例子:

BEGIN TRAN UPDATE Region SET RegionDescription = 'South' WHERE RegionID = 4

Region是一个堆表,在RegionId上只有非聚集唯一索引主键。因此完成上面查询时,SQL Server在RegionId上扫描索引, 锁住索引页和索引键。当发现要改变得记录行后,因为更新查询并不改变RegionId的值,因此不会升级到排它锁。 运行SP_LOCK后可以得到以下信息:

我们看到,在RID上有一个IX锁。该锁位于RegionId索引上。还可以看到在表上有一个IX锁,RID上有一个X锁。 KEY锁在RegionId索引上,证据可以从Indid列上可以得到。在索引上还有一个更新锁,这是更新锁激活的一个瞬间之一。

当查询结束后,仍然存在两个页面锁 –- 一个在索引页 (1:306)上, 另一个在堆(heap) (1:300)上。这是因为 堆的Indid(Index id)为0。

锁粒度(Lock Granularity)

SQL Server有几种锁类型,每种类型都可以选择不同的粒度。

如果运行SP_LOCK,或者查看企业管理器中\当前激活\信息,就可以看到至少四,五中不同的锁类型。下面简单 回顾一下这些类型:

?

?

Database (DB): 这是一种会话(session)锁。例如,它不涉及任何事务,仅仅是一个用户和数据库之间的连接。 这样就可以防止有用户连接到数据库时,该数据库被卸载了。值得注意的是,虽然SQL Server的master和tempdb是 不能卸载的,但是在这两个数据库上是没有DB锁的。

Table (TAB): 这是SQL Server中最粗略的逻辑锁。在表级别上经常加的是意向锁 (觉得意向锁不安全吗? 这里有更详细的信息。)