8.3 锁定(Locking)
对于多用户数据库系统(包括SQL Server)而言,锁定是一个至关重要的功能。锁在悲观和乐观并发控制模型中都有所应用,尽管在每种模型中其他进程处理被锁定数据的方式是不同的。在本书中之所以要将悲观版的已提交读隔离称为“已提交读(锁定)”,是因为锁定使得并发事务能够维持一致性。在悲观模型中,写者总是阻塞读者和写者,而读者也会阻塞写者。对于乐观模型,唯一可能发生的阻塞是写者阻塞其他写者。但是为了真正理解这些简化的行为小结意味着什么,我们需要深入了解一下SQL Server 的锁定机制。
锁定的基本概念
SQL Server 可以使用几种不同方式来锁定数据。举例来说,读操作获取共享锁而写操作获取排他锁。更新锁(update locks)在更新操作(当SQL Server 要寻找特定数据进行更新时)的开头部分被获取。SQL Server 会自动获取并释放所有这些类型的锁。它还负责管理锁定模式之间的兼容性,解决死锁问题,并在需要的时候进行锁升级(escalate locks)。它在表、表的分页、索引键以及单独的数据行上支配锁。锁还可以加在系统数据上(对于数据库系统专用的(private)数据,譬如分页标头以及索引)。
SQL Server 提供两种独立的锁定体系。第一种体系影响所有完全共享的数据并提供行级锁、分页锁、表、数据分页、LOB 分页以及索引分页叶级别上的表级锁。第二种体
系是SQL Server 内部使用的,用来处理索引并发控制,管理对于内部数据结构的访问以及获取数据分页中的个别记录。第二种系统采用了闩(latch),比锁要少耗资源并能提供性能优化。可以使用成熟的(full-blown)锁来处理所有锁定机制,但由于其复杂性,如果将其用于所有的内部需求的话会减慢系统。如果通过使用系统存储过程sp_lock来察看锁的情况,或者通过类似机制的视图sys.dm_tran_locks 来得到锁信息的话,是看不见闩的——只能看到有关锁的信息。
另一种看待锁和闩之间的区别是:锁保证数据的逻辑一致性,而闩则保证物理一致性。当用户将一行记录物理地放置到一个分页上或者通过其他方式来移动数据(譬如在一个分页上压缩空间),闩锁(latching)就会发生了。SQL Server 必须保证这类数据移动能够不受干扰地发生。
自旋锁(Spinlocks)
基于较为短期的需求,SQL Server 用自旋锁来实现互斥(mutual exclusion)。自旋锁纯粹是为了互斥而设计的,并且不会锁住用户数据。它们比闩更加轻量(闩比用来锁定数据和索引叶子分页的完全锁要更加轻量)。如果锁(lock)不是马上可用的话,自旋锁的请求者会反复进行申请(也就是说,申请者会在锁上不断“旋转”,直到它可用为止)。
在SQL Server 中,自旋锁经常是用作互斥体(mutex)的,主要是针对那些通常不是很忙的资源。由于自旋锁的持续时间足够短,如果一个资源被占用的话,重新尝试会比默默等待要更加合适并且之后会由操作系统进行重新调度,最终导致线程之间的上下文切换(context switching)。只要不必自旋太长久,上下文切换所节省的成本要超过自旋所抵消的成本。自旋锁适用于等待资源的预期时间十分短暂的情况(或者无预期等待的情况)。
用户数据的锁定类型
下面将讨论有关锁定用户数据的四个方面的议题。首先会了解到锁定的各种模式(锁定类型)。在前文已经提到过共享锁、排他锁和更新锁,接下来将更深入地研究它们及其他模式。然后将了解锁的粒度(granularity),它规定了一个单独的锁能够覆盖多少数据。这可以是一行、一个分页、一个索引键、索引键的一个范围、一个扩展(extent)抑或是整张表。关于锁定的第三个主题是锁的持续时间(duration)。之前提到过,一些锁在数据被访问完成以后会立刻释放掉,并且某些锁会保留到事务提交或者回滚为止。有关锁定的第四个主题会涉及到锁的所有权问题(锁的范畴scope)。锁可以被一个会话、事务或者游标所拥有。
锁的模式
SQL Server 采用了几种锁定模式,包括共享锁、排他锁、更新锁以及意向锁,再加上这些类型的一些变种。锁的模式决定了一个并发请求的锁是否兼容(已经存在的锁)。在本节末尾我们将看到一张锁兼容性矩阵的图表。
共享锁(shared locks)
当数据被读取时,SQL Server 自动获取共享锁。共享锁可以被一张表、一个分页、一个索引键或者一个单独的行所持有。许多进程可以在同一数据上都持有共享锁,但是没有进程可以在已经有一个共享锁存在的情况下,在该数据上再获得一个排他锁(除非申请排他锁的进程和持有共享锁的进程是同一个)。一般地,当数据已经读取完毕后,共享锁就会立即释放掉,但是可以通过使用查询提示(query hints)或者采用不同的事务隔离级别来改变这种(默认)方式。
排他锁(exclusive locks)
当数据被插入、更新或者删除操作修改以后,SQL Server 就会自动获取数据上的排他锁。一次只能有一个进程持有特定数据资源上的排他锁。实际上,正如在后面“锁兼容性”一节中所述,如果别的进程已经排他性地锁定住某个进程所要申请的数据资源,那么该进程就无法获取任何类型的锁了。排他锁会保留到事务结束为止。这就意味着被修改的数据通常在当前事务提交或者回滚之前对其他进程来说是不可用的。其他进程可以通过使用查询提示来读取被排他性锁定的数据。
更新锁(update locks)
更新锁实际上并不是一种独立的锁,它们是共享锁和排他锁的一种混合(hybrid)。当SQL Server 执行一个数据修改操作但首先需要搜索表以寻找到要被修改的资源时,更新锁就会被获取了。通过使用查询提示,一个进程可以明确地申请更新锁,而在这种情况下,更新锁就能够预防本章稍后在图8-6 中所呈现的转换死锁(conversion deadlock)的情况了。
更新锁提供对当前其他数据读者的兼容性,在确保数据自上次读取以后尚未修改的前提下,使进程能够在稍后对数据进行修改。更新锁本身是不足以使用户能够修改数据的——所有的数据修改都要求被修改的数据资源上存在一个排他锁。更新锁的作用就好像一个串行化闸门(serialization gate),将后续申请排他锁的请求压入队列中(许多进程都可以对一个资源持有共享锁,但是只有几个进程能够持有更新锁)。只要有一个进程对资源持有更新锁,其他进程就无法获取该资源的更新锁或者排他锁了,而其他正在申请相同资源上更新锁或者排他锁的进程就必须等待了。持有更新锁的进程能够将其
转换成该资源上的排他锁,因为更新锁避免了与其他进程之间的锁不兼容性。可以将更新锁看作是“意图更新”(intent-to-update)锁,在根本上也就是它们所扮演的角色了。如果单独使用的话,更新锁是不足以更新数据的——仍然需要获取排他锁来进行实际的数据修改。对于排他锁的依次访问可以避免转换死锁(conversion deadlock)的发生。更新锁会保留到事务结束或者当它们转换成排他锁时为止。
不要被名字所误导了:更新锁并不只是针对更新操作而设计的。SQL Server 使用更新锁适用于任何需要在进行实际修改之前搜索数据的数据修改操作。这样的操作包括受限更新及删除,也包括在带有聚集索引的表上进行的插入操作。对于后面一种情况,SQL Server 必须先搜索数据(使用聚集索引)以找到正确的位置来插入新的记录。当SQL Server 只进行到搜索阶段时,它会采用更新锁来保护数据,而只有当它找到正确的位置并开始插入以后才将更新锁升级成排他锁。
意向锁(intent locks)
意向锁实际上并不是一种独立的锁定模式,它们是之前讨论过的那些模式的限定词(qualifier)。换句话说,可以拥有意向共享锁、意向排他锁甚至意向更新锁。由于SQL Server 可以在不同级别的粒度上获取锁,因此需要一种机制来指出一个资源上的组件已经被锁定住了。举例来说,如果一个进程试图锁定一张表,SQL Server 就需要采用一种方式来判断这张表上的一行(或者一个分页)已经被锁定住了。意向锁就是起这个作用的。在了解锁的粒度时还会深入研究意向锁。
特殊锁定模式
SQL Server 提供了三种其他类型的锁定模式:架构稳定锁(schema stability locks)、架构修改锁(schema modification locks)以及大容量更新锁(bulk update locks)。当查询被编译时,架构稳定锁会防止其他进程获取架构修改锁(在表结构被修改时获得)。在执行BULK INSERT 命令或者使用bcp 工具将数据导入表时就会获取大容量更新锁。另外,大容量导入操作必须使用TABLOCK 查询提示来请求这个特殊的锁。或者,可以将表的table lock on bulk load 选项设为true,然后任何大容量导入(bulk copy IN)或者BULK INSERT 操作都会自动申请大容量更新锁。申请这种特殊的大容量更新表级锁不一定代表会授予(grant)这个锁。如果其他进程已经持有了表上的锁,或者表上有索引存在,就不会授予大容量更新锁了。如果有多个连接已经申请并得到了一个大容量更新锁,它们可以执行并行调用将数据导入相同的表中。与排他锁不同的是,大容量更新锁之间不会互相冲突,因此多连接的并发插入在SQL Server 中是支持的。
转换锁(conversion locks)
SQL Server 不会直接申请转换锁,而是由一种模式转换成另一种模式所造成的。SQL Server 2005 所支持的三类转换锁是:SIX、SIU 以及UIX。其中最常见的是SIX,如果事务正持有一个资源上的共享锁(S)并在稍后需要一个IX 锁时产生,这种锁定模式可以简写为SIX。譬如,假设用户正处于可重复读的事务隔离级别并执行了如下的批(batch):
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
BEGIN TRAN
SELECT * FROM bigtable
UPDATE bigtable
SET col = 0
WHERE keycolumn = 100
如果表很大,SELECT 语句会获取一个共享表级锁(如果该表只有一些记录,SQL Server 会获取单个的行级锁或键锁)。接着,UPDATE 语句会获取一个单独的排他锁来执行单行记录的更新,而键级上的X 锁意味着在分页和表级上存在IX 锁。当通过sys.dm_tran_locks 视图察看时会显示该表上的SIX 锁。相类似地,当一个进程在一张表上拥有一个共享锁并且在该表的一行记录上拥有一个更新锁时,SIU 锁就会产生了;而当一个进程在一张表上拥有一个更新锁并在一行记录上拥有一个排他锁时,UIX 锁就产生了。
表8-3 显示了大多数锁定模式,包括在sys.dm_tran_locks 中使用的缩写。
表8-3 SQL Server 的锁定模式
|
缩写 |
锁定模式 |
描述 |
|
S |
Shared |
允许其他用户读取但不能修改被锁定资源 |
|
X |
Exclusive |
防止别的进程修改或者读取被锁定资源的数据(除非该进程设定为未提交读隔离级别) |
|
U |
Update |
防止其他进程获取更新锁或者排他锁;在搜索数据并修改时使用 |
|
IS |
Intent shared |
表示该资源的一个组件被一个共享锁锁定住了。这类锁只能在表级或者分页级才能被获取 |
|
IU |
Intent update |
表示该资源的一个组件被一个更新锁锁定住了。这类锁只能在表级或者分页级才能被获取 |
|
IX |
Intent exclusive |
表示该资源的一个组件被一个排他锁锁定住了。这类锁只能在表级或者分页级才能被获取 |
|
SIX |
Shared with intent exclusive |
表示一个正持有共享锁的资源还有一个组件(一个分页或者一行记录)被一个排他锁锁定住了 |
续表
|
缩写 |
锁定模式 |
描述 |
|
SIU |
Shared with intent update |
表示一个正持有共享锁的资源还有一个组件(一个分页或者一行记录)被一个更新锁锁定住了 |
|
UIX |
Update with intent exclusive |
表示一个正持有更新锁的资源还有一个组件(一个分页或者一行记录)被一个排他锁锁定住了 |
|
Sch-S |
Schema stability |
表示一个使用该表的查询正在被编译 |
|
Sch-M |
Schema modification |
表示表的结构正在被修改 |
|
BU |
Bulk update |
在一个大容量复制操作将数据导入表并且应用了TABLOCK 查询提示时使用(手动或者自动皆可) |
键范围锁(key-range locks)
另外的锁定模式(称为键范围锁)只在可串行化隔离级别中为了锁定一定范围内的数据而被获取。大多数锁定模式能够应用于几乎任何锁资源上。举例来说,共享锁和排他锁可以在表、分页、行或者键上获取。因为键范围锁只能从键上获取,我们将在稍后的“键锁”一节中深入了解键范围锁。
锁的粒度(lock granularity)
SQL Server 可以锁定表、分页、行等级别的用户数据资源(非系统资源;而系统资源是由闩来保护的)。它同样可以锁定索引键以及一定范围内的索引键。图8-1 显示了表上可能存在的加锁级别。谨记如果表上存在聚集索引,数据行就在聚集索引的叶级别并且是由键锁而不是行锁来锁定它们的。

图8-1 表上的SQL Server 加锁粒度级别
sys.dm_tran_locks 视图对每个锁都进行追踪并且包含了被锁定资源(譬如行、键或者分页)、锁的模式以及特定资源的一个标识符(identifier)。谨记:sys.dm_tran_locks 只是一个用来显示所持有锁的动态管理视图。实际的信息是储存在SQL Server 内部的数据结构中,对用户来说是完全不可见的。因此,当谈论到sys.dm_tran_locks 视图中所存在的信息时,实际上指的就是通过该视图能够看到的信息。
当一个进程申请锁时,SQL Server 会将所申请的锁与sys.dm_tran_locks 中已经列出的资源相比较并寻找完全匹配资源类型(resource_type)以及标识符(identifier)的锁。但是,譬如说,如果一个进程在sales.SalesOrderHeader 表中的某行上占有一个排他锁,别的进程可能会尝试在整张sales.SalesOrderHeader 表上获取一个锁。由于是两种不同的资源,SQL Server 不会找到一个完全的匹配,除非额外信息已经存在于sys.dm_tran_locks 之中。这就是意向锁存在的理由了。在sales.SalesOrderHeader 表的一行记录上拥有排他锁的进程也在包含该行记录的分页上占有一个意向排他锁,以及在包含该行记录的这张表上拥有另一个意向排他锁。可以先通过运行以下的代码来观察这些锁:
USE AdventureWorks;
BEGIN TRAN
UPDATE Sales.SalesOrderHeader
SET ShipDate = ShipDate + 1
WHERE SalesOrderID = 43666;
这条语句应该影响到一个单独的行。由于启动了一个事务并且尚未结束,所有排他锁仍旧被占有着。可以使用sys.dm_tran_locks 视图来察看这些锁:
SELECT resource_type, resource_description,
resource_associated_entity_id, request_mode, request_status
FROM sys.dm_tran_locks
WHERE resource_associated_entity_id > 0
在本章稍后部分将介绍更多关于sys.dm_tran_locks 视图内的信息,但是到目前为止,我只需要指出关于使用WHERE 子句中的筛选部分的原因:我们只需关注实际加在数据资源上的锁。如果在一个多人使用的SQL Server 实例上执行一个查询,可能需要通过更多筛选来得到自己感兴趣的信息。举例来说,可以在request_session_id 字段上加以筛选来限制由某个特定会话所持有的锁的输出结果。这个查询结果应该看上去类似于如下结果:
resource_type resource_description resource_associated_entity_id request_mode request_status
------------- -------------------- ----------------------------- ------------ -------------
KEY (92007ad11d1d) 72057594045857792 X GRANT
PAGE 1:5280 72057594045857792 IX GRANT
OBJECT 722101613 IX GRANT
注意:即使UPDATE 语句只影响了一个单独的行,也有三个锁存在。对于KEY 和PAGE 锁来说,resource_associated_entity_id 就是allocation_unit_id。对于OBJECT 锁
而言,resource_associated_entity_id 就是表本身(表的object id)。可以通过如下查询来验证这究竟是哪张表:
SELECT object_name(722101613)
该结果应该表明这个对象就是SalesOrderHeader 表。当第二个进程试图获取这张表上的一个排他锁时,它会在sys.dm_tran_locks 中的相同锁资源上找到一个已经存在的冲突行,而此进程就会被阻塞。sys.dm_tran_locks 视图显示出下面这行结果,表示在该对象上的排他锁申请还无法得到允许(granted)。这个请求锁的进程会处于一个WAIT 状态。
resource_type resource_description resource_associated_entity_id request_mode request_status
------------- -------------------- ------------------------------ ------------ -------------
OBJECT 722101613 X WAIT
并不是所有在已经被锁定的资源上的锁申请会导致冲突。当一个进程要在已经被别的进程锁定的资源上申请不相兼容的锁时会产生冲突。譬如说,两个进程都可以在同一资源上获取共享锁,因为共享锁之间是互相兼容的。在本章稍后部分将详细介绍锁的兼容性。
键锁(key locks)
SQL Server 2005 支持两种类型的键锁,而它究竟采用哪种类型则取决于当前事务的隔离级别。如果隔离级别是已提交读、可重复读或者快照,SQL Server 会在处理查询时尝试锁定实际被访问的索引键。对于拥有聚集索引的表而言,数据行就是索引的叶级别,而用户可以看到所获得的键锁。如果表是堆结构(heap)的话,用户可能会看到非聚集索引上的键锁以及实际数据上的行锁。
如果隔离级别是可串行化,情况就有所不同了。为了防止幻影,如果在一个事务中扫描了一个范围内的数据就需要充分锁定住该表以确保没人能够插入新值到已扫描的范围内。举例来说,可以在AdventureWorks 数据库中发起一个显式事务并执行如下查询:
BEGIN TRAN
SELECT * FROM Sales.SalesOrderHeader
WHERE CustomerID BETWEN 100 and 110;
当使用可串行化隔离时,必须获取锁以确保在事务结束之前没有CustomerID 值介于100 和 110 的新记录被插入。在许多SQL Server 的早期版本中(7.0 以前)是通过锁定整个分页甚至是整张表来保证这一点的。在许多情况下,这是具有相当大的限制的——更多的数据(比WHERE 子句所表示的实际数据要多)被锁定住,造成不必要的(资源)
竞争。SQL Server 2005 采用了一种称为“键范围锁”的单独锁定模式,与索引中的特定键值相关联并表明在索引中这个键与上一个键之间的所有值都被锁定住了。
AdventureWorks 数据库包含了一个在Person.Contact 表的LastName 字段上的索引。假设当前正处于TRANSACTION ISOLATION LEVEL SERIALIZABLE 并且执行了下面的SELECT 语句:
SELECT * FROM Person.Contact
WHERE LastName BETWEEN 'Freller' AND 'Freund';
如果Fredericksen、French 以及 Friedland 在LastName 字段的索引中是连续的叶级别索引键,而其中后面的两个(French 和 Friedland)获得键范围锁(尽管只有一行记录French 返回到结果集中)。键范围锁可以防止在以这两个锁为端点的范围内进行任何插入操作。没有大于Fredericksen 且小于或等于French的值能够被插入,并且没有大于French 且小于或等于Friedland 的值能被插入。注意:键范围锁涵盖了从前一个连续键开始的开区间(open interval)到放置锁的那个键为止的闭区间。这两个键范围锁可以防止其他人插入Fremlich 或者 Frenkin(介于WHERE 子句所指定的范围之内)。然而,键范围锁还会防止其他人插入Freedman (Freedman 大于Fredericksen 小于French),即使Freedman 并不在查询所指定的范围之内。键范围锁并非完美,但它们的确在保证幻影不会出现的前提下比锁定整个分页或表提供了更高的并发能力。
一共有九种类型的键范围锁,而其中每一个都有一个二段式的名字:第一段表示相邻索引键之间的数据范围上的锁定类型,而第二段表示键本身的锁定类型。表8-4 描述了这九种键范围锁。
表8-4 键范围锁的类型
|
缩写 |
描述 |
|
RangeS-S |
键之间的范围上是共享锁;范围的端点(键本身)上是共享锁 |
|
RangeS-U |
键之间的范围上是共享锁;范围的端点上是更新锁 |
|
RangeIn-Null |
键之间的范围上是用来防止插入的排他锁;键本身没有锁 |
|
RangeX-X |
键之间的范围上是排他锁;范围的端点上是排他锁 |
|
RangeIn-S |
由S 锁和RangeIn_Null 锁创建的转换锁 |
|
RangeIn-U |
由U 锁和RangeIn_Null 锁创建的转换锁 |
|
RangeIn-X |
由X锁和 RangeIn_Null 锁创建的转换锁 |
|
RangeX-S |
由RangeIn_Null 锁和RangeS-S 锁创建的转换锁 |
|
RangeX-U |
由RangeIn_Null 锁和RangeS-U 锁创建的转换锁 |
这其中的许多锁定模式都是非常罕见或者持续时间非常短暂的,因此在sys.dm_tran_locks 也不是经常能见到的。举例来说,在一个使用可串行化隔离的会话中,RangeIn-Null 锁会在SQL Server 尝试插入值到键之间的范围内时被获取。这种类型的锁并不常见,因为它的持续时间总是非常短暂的。这类锁只会保留到正确的插入位置被发现为止,而紧接着它就会升级为X 锁。然而,如果在可串行化隔离级别下,一个事务扫描了一个范围内的数据而紧接着另一个事务尝试在这个范围内插入数据,这时第二个事务会发起一个状态为WAIT、 模式为RangeIn-Null 的锁定请求。可以通过察看sys.dm_tran_locks 视图中的status 字段来观察这个行为,而本章稍后部分也将深入讨论其细节。
另外的可锁定资源
除了在对象、分页、键以及行上加锁以外,SQL Server 还可以锁定一些其他的资源。扩展(磁盘空间上具有64 KB 大小的分配单元,由八个8 KB 大小的分页组成)上也可以加锁。当一个表或者索引需要增长且必须分配一个新的扩展时,这类锁定就会自动发生。可以把扩展锁想象成另一种类型的具有特殊用途的闩(latch),但是它不会在sys.dm_tran_locks 中出现。扩展可以拥有共享扩展锁和排他扩展锁。
当察看sys.dm_tran_locks 的内容时,应该会注意到大多数进程会至少在一个数据库上(resource_type = 'DATABASE')持有锁。实际上,在除了master 和 tempdb 以外的任何数据库上的所有持锁进程都会在该数据库资源上拥有一个锁。如果进程只是使用(use)数据库,这些数据库级锁总是共享锁。当要判断一个数据库是否正被使用并且接着可以判断该数据库是否能够被删除(drop)、还原(restore)、修改(alter)或者关闭(close)时,SQL Server 就会去检查这些数据库级锁。由于很少对master 和tempdb 数据库进行改变而且它们也是无法删除或者关闭的,数据库级锁对它们而言就是多余的了。当尝试执行这些操作时,SQL Server 会申请一个数据库级的排他锁,而如果有任何其他进程在该数据库上持有共享锁,这个请求就会被阻塞住。一般地,用户不必关心扩展锁或数据库级锁,但如果细读sys.dm_tran_locks 的内容,的确会看到它们。
可能偶尔还会在HOBT 和ALLOCATION_UNIT 资源上看到锁的存在。尽管所有表和索引结构都是基于HOTBT 且包含了一个或多个ALLOCATION_UNIT,当这些锁定发生的时候,就意味着SQL Server 正在处理这其中的某个不再与一个特定对象关联的资源。举例来说,当删除或者重建庞大的表或索引时,实际的分页回收(deallocation)操作是被推迟到事务提交以后才进行的。延迟(deferred)的删除(drop)操作并不立即释放已分配空间,而且还引入了额外开销,因此延迟的删除操作只在使用了超过128 个扩展的表或索引上完成。如果表或索引使用了128 个或更少的扩展,删除、截断(truncate)以及重建操作的执行就和SQL Server 2005 之前的版本一样,不会有延迟操作发生。在延迟操作的首个阶段里,表或者索引所使用的现存分配单元(existing allocation units)会
被标记为提示回收(deallocation),并且被锁定到事务提交为止。这就是在sys.dm_tran_locks 中看到ALLOCATION_UNIT 锁的原因。还可以在sys.allocation_units 视图中找到type_desc 值为DROPPED 的分配单元,并看到有多少空间被分配单元占用,导致这部分空间既无法被重用又不属于任何对象。分配单元空间的实际物理删除操作会在事务提交以后发生。
应用程序级锁(application lock)
SQL Server 储存锁定信息及检查不兼容锁所采用的方法是相当直观和可扩展的。SQL Server 并不知道被它锁定的对象。它仅仅是处理一些代表资源的字符串,并不知道该对象的实际结构。如果两个进程试图在同一资源上获取不相兼容的锁就会发生阻塞。
应用程序级锁使用户能够利用SQL Server提供的检测阻塞和死锁情况的机制,并且可以选择锁定住任何想要锁住的东西。这些可锁定资源就是应用程序级锁。要定义一个应用程序级锁,就要为想要锁定的资源指定一个名字、锁的模式、所有者(或者范畴)、超时设定以及一个数据库主体ID (Database Principal ID,也就是数据库中能够拥有权限的一个用户、角色、应用程序角色等)。与SQL Server 自己的可锁定资源(如表和分页等)不同的是,应用程序级锁必须进行明确地申请。只有符合以下标准之一的用户才能够执行sp_getapplock 存储过程:
n 用户(user)是dbo。
n 用户属于 db_owner 角色。
n 用户属于指定的数据库主体ID 角色(如果ID 是一个个体的名字)。
n 用户属于指定的数据库主体ID 角色(如果ID 是一个角色)。
注意 当使用术语“应用程序级锁”时,锁的拥有者与请求/持有该锁的用户是无关的。也可以将它理解成锁的范畴。稍后将继续讨论锁的拥有者。
默认的数据库主体ID 是public(可以调用sp_getapplock 察看)。
如果两个资源在同一数据库中具有相同名字和相同的数据库主体ID,它们就被视为相同的资源并且都受限于阻塞。对于应用程序级锁,锁的拥有者既可以是会话也可以是事务。如果被申请锁模式之间是互相兼容的话,在同一资源上的两个锁申就请都会被批准。检查锁兼容性时,使用的是为SQL Server 内建锁而设计的相同的兼容性矩阵。
举例来说,假设用户有一个存储过程且每次只有一个用户执行。属于ProcUserRole 数据库角色的任何用户都可以通过sp_getapplock 存储过程来获取一个特殊锁(告诉
其他进程:已经有人在使用该存储过程了)以锁定这个存储过程。当存储过程执行完毕以后,可以使用sp_releaseapplock 来释放这个锁:
EXEC sp_getapplock 'ProcLock', 'Exclusive', 'session', 'ProcUserRole'
EXEC MySpecialProc <parameter list>
EXEC sp_releaseapplock 'ProcLock', 'session'
直到通过sp_releaseapplock 释放掉该锁以后,或者到会话结束为止,如果它遵循此协议并且在尝试执行存储过程以前使用sp_getapplock 来申请名为ProcLock 的资源,任何会话就不能执行该存储过程了。用户可以自由为其选择标识符。SQL Server 只是在申请应用程序锁时往sys.dm_tran_locks 视图中添加一行记录,并且通过资源名称和数据库主体ID 来与其他已经申请过的锁进行比较。注意存储过程本身实际上并没有被锁定住。如果别的用户或应用程序不知道这是一个特殊的存储过程,并且试图不通过获取应用程序锁来执行MySpecialProc,SQL Server 并不会阻止该会话执行这个存储过程。
在这些存储过程中用到的资源名称可以是任何不超过255 个字符长度的标识符。然而,只有前32 个字符会显示在sys.dm_tran_locks 的resource_description 字符串中。这种锁的模式可能有共享、排他、更新、意向排他以及意向共享等,用来检查与其他申请此相同资源请求的兼容性。没有默认的模式,用户必须指定一个。第三个参数,锁的所有者(范畴)可能是事务(默认值)或者会话。如果锁的所有者是事务,必须在用户定义的事务之内获取锁,它会在事务结束时自动释放而不必调用sp_releaseapplock。如果锁的所有者是会话,它只会在会话断开(disconnect)时自动释放。
下面是一个例子。用前面提到的语句申请应用程序锁并通过sys.dm_tran_locks 来观察:
EXEC sp_getapplock 'ProcLock', 'Exclusive', 'session';
GO
SELECT resource_type, resource_description,
resource_associated_entity_id, request_mode, request_status
FROM sys.dm_tran_locks
WHERE resource_type = 'APPLICATION'
结果如下:
resource_type resource_description resource_associated_entity_id request_mode request_status
------------ -------------------- ----------------------------- ------------ ------------
APPLICATION 0:[ProcLock]:(8e14701f) 0 X GRANT
注意没有任何数据库实体与该锁关联,因为SQL Server 并不将其连接到数据库中任何实际存在的资源上。
鉴别锁定资源
当SQL Server 要决定是否可以授予一个申请的锁时,会检查sys.dm_tran_locks 视图来判断是否已经有处于冲突锁模式的匹配锁存在了。它通过察看数据库ID(resource _database_ID)、resource_description 、resource_associated_entity_id 字段中的值以及被锁定资源的类型来比较锁。SQL Server并不了解资源描述中的含义。它只是比较识别锁资源的字段来寻找一个匹配。如果发现一个匹配且request_status 值是GRANT,它就知道资源已经被锁定住了。然后SQL Server会利用锁兼容性矩阵来判断当前的锁是否在正申请的锁中兼容。表8-5 列出了许多在sys.dm_tran_locks 视图的第一个字段中显示的可能的锁资源,而resource_description 字段中的信息是用来定义实际被锁资源的。
表8-5 SQL Server 中可锁定的资源
|
资源类型 |
资源描述(Resource_Description) |
例子 |
|
DATABASE |
无;每个被锁资源的resource_database_ID 字段都指明了数据库 |
|
|
OBJECT |
对象ID (可以是任何数据库对象,不一定就是表),其数值是由resource_associated_entity_id 字段反馈的 |
69575286 |
|
EXTENT |
扩展(extent)的第一个分页的文件号:页号 |
1:96 |
|
PAGE |
实际表或者索引分页的文件号:分页号 |
1:104 |
|
KEY |
由所有键的组成部分及定位符得到的哈希值。对于一个建在堆上的非聚集索引(c1 和c2 是索引列),哈希将包含来自c1,c2以及RID 的贡献 |
ac0001a10a00 |
|
ROW |
实际数据行的文件号:页号:槽号 |
1:161:3 |
|
APPLICATION |
一个连接串,由以下几部分组成:有权访问该锁的数据库主体、锁名的前32个字符以及根据该锁全名得到的哈希值 |
0:[ProcLock]:(8e14701f) |
注意:键锁和键范围锁具有相同的资源描述,因为键范围被视为一种锁定模式,而不是一种锁定资源。当察看sys.dm_tran_locks 视图的输出结果时,可以通过锁模式字段的值来区分各种类型的锁。
其他可能列于resource_type 字段的资源还包括之前提过的HOBT和ALLOCATION _UNIT。可以通过下一小节中要介绍的resource_associated_entity_id 的值来鉴别。最后一种可锁定的资源是METADATA(元数据)。METADATA 资源被划分成多个子类型(比任何其他资源都要多),由sys.dm_tran_locks 视图的resource_subtype 字段来描述。你可能会发现METADATA 资源的许多子类型,但其中的大多数已经超出了本书的范围。对于其中的一些,即使在SQL Server 联机丛书中将其描述为“仅供内部使用”,它们所指的含义还是相当明显的。譬如,当改变数据库的属性时,可以(在sys.dm_tran_locks 中)看到一个资源类型(resource_type)为METADATA、资源子类型(resource_subtype)为DATABASE 的记录。这条记录的resource_description 字段值是database_id =<ID>,表示当前元数据被锁住的数据库ID。
关联实体ID(Associated Entity ID)
对于一个较大实体内的部分被锁资源,sys.dm_tran_locks 中的resource_associated _entity_id 字段显示了数据库中的那个关联实体的ID。它可以是对象ID、HOBT ID 或者分配单元ID,这取决于资源类型。当然,对于某些资源,像DATABASE 和EXTENT,就没有对应的resource_associated_entity_id 了。对于OBJECT 类型的资源,该字段中指定了对象ID 值,而对于ALLOCATION_UNIT 资源,该字段指定了分配单元的ID。资源类型PAGE、KEY、RID 和HOBT提供了一个HoBT ID视图。
没有简单的函数可以将HoBT ID 值转换成对象名,实际上必须从sys.partitions 视图中选取。下面的查询通过将sys.dm_tran_locks 视图连接到sys.partitions视图, 对当前数据库中全部锁的resource_associated_entity_id 值进行转译。对于OB JECT 型资源,要在resource_associated_entity_id字段上应用object_name 函数。对于PAGE、KEY 和RID 型资源,要在从sys.partitions 视图中得到的对象ID 上应用object_name。对于其他没有resource_associated_entity_id 的资源,这段代码就只返回n/a。由于object_name 函数只适用于当前数据库,这段代码筛选并且只返回当前数据库中资源的锁定信息。其输出是按照sp_lock 存储过程返回信息的格式来组织的,但读者可以自行添加任何额外的过滤或者所需的字段。在本章稍后部分还会在许多例子中用到这个查询,所以我们需要基于这个SELECT语句新建一个名为DBlocks 的视图。
CREATE VIEW DBlocks AS
SELECT request_session_id as spid,
db_name(resource_database_id) as dbname,
CASE
WHEN resource_type = 'OBJECT' THEN
object_name(resource_associated_entity_id)
WHEN resource_associated_entity_id = 0 THEN 'n/a'
ELSE object_name(p.object_id)
END as entity_name, index_id,
resource_type as resource,
resource_description as description,
request_mode as mode, request_status as status
FROM sys.dm_tran_locks t LEFT JOIN sys.partitions p
ON p.hobt_id = t.resource_associated_entity_id
WHERE resource_database_id = db_id();
锁的持续时间
锁的持续时间主要取决于锁的模式和当前作用的事务隔离级别。SQL Server 的默认隔离级别是已提交读。在该级别下,SQL Server 会在读取和处理完锁定数据以后立刻释放共享锁。对于快照隔离级别,其行为也是相同的——SQL Server 读完数据后立即释放共享锁。如果事务隔离级别是可重复读或者可串行化,共享锁就和排他锁的持续时间相同。也就是说,直到事务结束时才会被释放。对于任何隔离级别,无论事务是被提交还是被回滚,排他锁都会一直持续到事务结束为止。更新锁也会像排他锁一样总是持续到事务结束,除非它已经被升级成排他锁了(在这种情况下的排他锁,其实对于排他锁来说总会保留到事务结束为止)。
除了改变事务隔离级别,还可以使用查询提示来控制锁的持续时间。在本章的稍后部分会简要讨论锁定的查询提示。对于提示(hints)的完整介绍请参考Inside Microsoft SQL Server 2005: Query Tuning and Optimization 一书。
锁的所有权(Ownership)
锁的持续时间也是受到锁的所有权直接影响的。锁的所有权与申请锁的过程没有关系,但可以将其想象成锁的“范畴”。一共存在四种锁的所有者(或范畴)类型:事务型、游标型、事务工作空间型(transaction_workspace)和会话型。可以通过sys.dm_tran_locks 视图的request_owner_type 字段来察看锁的所有者。
之前大多数的锁定讨论都与事务型的锁范畴有关。正如我们所见,这类锁可以拥有两个不同的持续时间(duration),这取决于事务的隔离级别以及锁定模式。已提交读隔离下的共享锁的持续时间只是被锁定数据被读取的时间。由事务所拥有的全部其他锁的持续时间要一直持续到事务结束为止。
req_owner_type的值为CURSOR 的锁必须在声明游标时显式地申请。如果一个游标是采用scroll_locks 锁定模式打开的话,每次被提取(fetched)的行记录上会持有一个游标锁,直到下一行记录被提取或者游标被关闭为止。即使事务在下一次提取之前就被提交了,游标锁也不会被释放掉。
在SQL Server 2005 中,由一个会话所拥有的锁也必须显式地申请并且只适用于应用程序锁。一个会话型锁是通过sp_getapplock 存储过程来申请的。它会一直持续到会话断开或者锁被显示地释放掉为止。
SQL Server 2000 较多地采用会话所有型锁,而在SQL Server 2005 中则采用事务工作空间型锁作为替代。一个工作空间是登记(enlisted)进入通用环境中的会话持有数据库级锁。通常地,每个会话对应一个工作空间,因此所有在会话中获取的数据库级锁会保留在相同的工作空间对象中。在分布式事务和绑定会话(bound session)的情况下(本章稍后部分介绍),多个会话会进入相同的工作空间,因而它们会共享数据库级锁。
当进程执行USE 命令时,会在一个数据库上获取一个所有者为SHARED_ TRANSACTION_WORKSPACE 的数据库级锁。例外发生在任何使用master 或者tempdb 数据库的进程上,在这种情况下不会获取数据库级锁。这个锁会到下一次USE 命令执行时或者进程断开时释放。如果一个进程试图修改、还原或者删除数据库,那么所获取的数据库级锁的所有者就是EXCLUSIVE_TRANSACTION_WORKSPACE。SHARED_ TRANSACTION_WORKSPACE和EXCLUSIVE_TRANSACTION_ WORKSPACE锁是由相同的工作空间维持的,并且是一个工作空间中的两个不同列表。在这种情况下,使用两种不同的所有者名容易引起误解。
观察锁
为了弄清系统当前的活动和等待状态的锁,最佳的来源信息是sys.dm_tran_locks 视图。在之前的小节中已经展示了一些基于该视图的查询,而在本小节中还会再介绍一些并说明其输出字段更多的含义。这个视图取代了(以前版本中的)sp_lock 存储过程。尽管调用存储过程可能比查询sys.dm_tran_locks 视图要少打一些字,但是视图要比存储过程灵活得多。sys.dm_tran_locks 不但包括了更多提供具体的锁信息的字段,而且作为一个视图,它可以在查询过程中只选择需要的字段或者符合某些条件的记录。它还能与其他视图连接(join)并聚合(aggregate),以得到每种类型的锁有多少这样的综述信息。
sys.dm_tran_locks
sys.dm_tran_locks 中的所有字段名都是以两种前缀中的一种来开头的。以resource_ 开头的字段对SQL Server正在其上申请锁的这个资源进行了描述。以request_ 开头的字段描述了申请过程。只有当所有的resource_ 字段内容相同时,才表示两个申请作用于相同的资源上。
资源字段 之前的章节中已经介绍过大多数的resource_ 字段,但其中只是简要地提到了resource_subtype 字段。并不是每种资源都有子类型,而其中的一些却有很多。譬如METADATA 资源类型,就有超过40种的子类型。
表8-6 列出了除METADATA 类型以外的其他资源的子类型。
表8-6 资源的子类型
|
资源类型 |
资源子类型 |
描述 |
|
DATABASE |
BULKOP_BACKUP_DB |
用于同步大容量操作的数据库备份 |
|
BULKOP_BACKUP_LOG |
用于同步大容量操作的数据库日志备份 |
|
|
DDL |
用于同步文件组的DDL 操作(如Drop 操作) |
|
|
STARTUP |
用于数据库启动同步 |
|
|
TABLE |
UPDSTATS |
用于表上的统计更新的同步 |
|
COMPILE |
用于存储过程编译的同步 |
|
|
INDEX_OPERATION |
用于索引操作的同步 |
|
|
HOBT |
INDEX_REORGANIZE |
用于堆或索引重组操作的同步 |
|
BULK_OPERATION |
用于并发扫描的堆优化(heap-optimized)大容量加载操作,适用于快照、未提交读和已提交读(快照)隔离级别 |
|
|
ALLOCATION_UNIT |
PAGE_COUNT |
用于延迟的删除操作(deferred drop operations)期间的分配单元分页数统计的同步 |
如前所述,大多数METADATA 子类型在文档中记录为“仅供内部使用”,但其含义通常都是十分明显的。当发生改变时,每种类型的元数据都能分别被锁定住。下面是部分METADATA 子类型的列表:
n INDEXSTATS。
n STATS。
n SCHEMA。
n DATABASE_PRINCIPAL。
n DB_PRINCIPAL_SID。
n USER_TYPE。
n DATA_SPACE。
n PARTITION_FUNCTION。
n DATABASE。
n SERVER_PRINCIPAL。
n SERVER。
大多数其他未列出的METADATA 子类型都是不在本书讨论范围之内的SQL Server 2005元素,包括CLR程序、XML认证、全文索引以及消息服务(notification services)等。
请求字段 之前也已提到了几个sys.dm_tran_locks 中最重要的request_ 字段,包括request_mode(申请锁的类型),request_owner_type(申请锁的范畴)以及request_session_id。这里再列举一些其他的:
n request_type 在SQL Server 2005 中,sys.dm_tran_locks 中追踪的唯一资源申请类型是锁(LOCK)。后续版本中还将包括其他可申请的资源类型。
n request_status 请求的当前状态可以是以下三个值中的一个:GRANT、CONVERT 或者WAIT。CONVERT 状态表示请求者已经被授予了相同资源上的一个不同模式的请求,所以当前正处于从所要授予的锁定模式进行升级(转换)的等待状态中(譬如,SQL Server 可以将一个U 锁转换成X锁)。WAIT 状态表示请求者当前还未在资源上持有一个已授权的请求。
n request_reference_count 该值返回同意请求程序已请求该资源的近似次数,并且仅适用于那些在事务结束时不会自动释放的资源。如果该值递减到0 并且request_lifetime 也是0 的话,一个已授权资源就不再被视作被某个请求者占有着了。
n request_lifetime 该值表示资源上的锁何时会释放。
n request_session_id 该值表示当前拥有该请求的会话ID。对于分布式事务(DTC)和绑定事务,拥有请求的会话ID 可能不同。该值为 -2 时,指示该请求属于孤立的分布式事务。该值为 -3 时,指示请求属于延迟的恢复事务(例如因其回滚未能成功完成而延迟恢复该回滚的事务)。
n request_exec_context_id 该值表示当前拥有该请求的进程的执行上下文ID。如果该值大于0,表示这是一个用于执行并行查询的子线程。
n request_request_id 该值是当前拥有该请求的进程的请求ID(批处理ID)。SQL Server只有在来自多个活动的结果集(MARS)的客户端应用程序的请求的情况下才会填充这个字段。
n request_owner_id 该值目前仅供所有者为事务的请求使用,且所有者ID 就是事务ID。该字段可以与sys.dm_tran_active_transactions 视图中的transaction_id 字段进行连接。
n request_owner_guid 该值目前仅供分布式事务使用,在该事务中,该值与事务的DTC GUID 相对应。
n lock_owner_address 该值用于跟踪该请求的内部数据结构的内存地址。如果请求处于WAIT 或者CONVERT 状态,该字段可以与sys.dm_os_waiting_tasks 中的resource_address 字段连接。
锁定的实例
下面的例子通过前述DBlocks 视图的输出结果演示了之前讨论过的许多锁定类型和模式。
实例1:默认隔离级别下的SELECT
SQL 批处理
USE AdventureWorks;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN TRAN
SELECT * FROM Production.Product
WHERE Name = 'Reflector';
SELECT * FROM DBlocks WHERE spid = @@spid;
COMMIT TRAN
DBlocks 的输出结果
spid dbname entity_name index_id resource description mode status
------- -------------- ------------ -------- ---------- ------------ ----- -----
60 AdventureWorks n/a NULL DATABASE S GRANT
60 AdventureWorks sysrowsets NULL OBJECT Sch-S GRANT
60 AdventureWorks DBlocks NULL OBJECT IS GRANT
Production.Product 表中的数据上没有锁存在,因为批处理只执行了SELECT 操作以获取共享锁。默认情况下,SQL Server 在读完数据以后立刻释放共享锁,因此当执行到DBlocks 视图的SELECT 语句时,(共享)锁已经不存在了。现在只有一个经常存在的数据库级锁、一个视图架构(schema)上的对象锁以及一个行集(rowset)上的对象锁了。
实例2:可重复读隔离级别下的SELECT
SQL 批处理
USE AdventureWorks;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRAN
SELECT * FROM Production.Product
WHERE Name LIKE 'Racing Socks%';
SELECT * FROM DBlocks
WHERE spid = @@spid
AND entity_name = 'Product';
COMMIT TRAN
DBlocks 的输出结果
spid dbname entity_name index_id resource description mode status
---- --------------- ------------ ---------- ---------- --------------- ---- -------
54 AdventureWorks Product NULL OBJECT IS GRANT
54 AdventureWorks Product 1 PAGE 1:16897 IS GRANT
54 AdventureWorks Product 1 KEY (6b00b8eeda30) S GRANT
54 AdventureWorks Product 1 KEY (6a00dd896688) S GRANT
54 AdventureWorks Product 3 KEY (9502d56a217e) S GRANT
54 AdventureWorks Product 3 PAGE 1:1767 IS GRANT
54 AdventureWorks Product 3 KEY (9602945b3a67) S GRANT
这次,将数据库锁以及视图和行集上的锁过滤掉,只关注数据上的锁。由于Production.Product 表上存在聚集索引,数据行就是叶级别的全部索引行。返回的两个数据行上的锁是键锁。该表的非聚集索引的叶级别上还有两个键锁,用来寻找相关数据。在Production.Product 表中,那个非聚集索引存在于Name 字段上。可以通过Index_ID 字段的值来区分聚集索引和非聚集索引:数据行对应的Index_ID 值为1,而非聚集索引行的Index_ID 值为3(对于非聚集索引,index_ID 可以是2 到250 之间的任意值)。由于事务隔离级别是可重复读,共享锁会持续到事务完成为止。注意:索引行拥有共享锁(S 锁)而数据分页、索引分页以及表本身拥有意向共享锁(IS 锁)。
实例3:可串行化隔离级别下的SELECT
SQL 批处理
USE AdventureWorks;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRAN
SELECT * FROM Production.Product
WHERE Name LIKE 'Racing Socks%';
SELECT * FROM DBlocks
WHERE spid = @@spid
AND entity_name = 'Product';
COMMIT TRAN
DBlocks 的输出结果
spid dbname entity_name index_id resource description mode status
---- -------------- ----------- -------- -------- ------------ ------- ------
54 AdventureWorks Product NULL OBJECT IS GRANT
54 AdventureWorks Product 1 PAGE 1:16897 IS GRANT
54 AdventureWorks Product 1 KEY (6b00b8eeda30) S GRANT
54 AdventureWorks Product 1 KEY (6a00dd896688) S GRANT
54 AdventureWorks Product 3 KEY (9502d56a217e) RangeS-S GRANT
54 AdventureWorks Product 3 PAGE 1:1767 IS GRANT
54 AdventureWorks Product 3 KEY (23027a50f6db) RangeS-S GRANT
54 AdventureWorks Product 3 KEY (9602945b3a67) RangeS-S GRANT
可串行化隔离级别下持有的锁几乎与可重复读隔离级别下的那些完全一致。主要的区别在锁定模式上。两部分模式RangeS-S 指示除了键自己身上的锁以外,还有一个键范围锁。第一部分(RangeS)是加在键值范围介于(且包含)持有锁的键及其在索引中的前一个键之间的锁。键范围锁阻止了其他事务向该表插入符合查询条件的记录,也就是说,不能够插入产品名以“Racing Socks”开头的新记录。键范围锁存在于Name 字段上的非聚集索引(IndId = 3)中的范围内,因为这就是用来寻找符合条件记录的索引。非聚集索引上共有三个键锁,因为需要锁定三个不同的范围。两个Racing Socks 记录分别是Racing Socks,L 和Racing Socks, M。SQL Server 必须锁定住从第一条Racing Socks 记录往前到它在索引中的上一个键之间的范围。还必须锁定住两条以Racing Socks 开头的记录之间的范围以及从第二条Racing Socks 记录往后直到它在索引中的下一个键之间的范围(因而,事实上在Racing Socks 到其上一个键Pinch Bolt以及Racing Socks 到其下一个键Rear Brakes 的范围之内都不能插入新的数据)。譬如说,我们无法插入一个名为Portkey 或Racing Tights 的新产品。
实例4:更新操作
SQL 批处理
USE AdventureWorks;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN TRAN
UPDATE Production.Product
SET ListPrice = ListPrice * 0.6
WHERE Name LIKE 'Racing Socks%';
SELECT * FROM DBlocks
WHERE spid = @@spid
AND entity_name = 'Product';
COMMIT TRAN
DBlocks 的输出结果
spid dbname entity_name index_id resource description mode status
---- --------------- ------------ -------- -------- ------------- ----- -----
54 AdventureWorks Product NULL OBJECT IX GRANT
54 AdventureWorks Product 1 PAGE 1:16897 IX GRANT
54 AdventureWorks Product 1 KEY (6b00b8eeda30) X GRANT
54 AdventureWorks Product 1 KEY (6a00dd896688) X GRANT
聚集索引叶级别中的两行记录被X 锁锁定住了。分页和表则被IX 锁锁定住。我们曾经提到过在寻找记录完成更新的时候,SQL Server 实际上获取的是更新锁。然而,当实际的更新操作完成时它们就被升级成X 锁了,因此当我们察看DBLocks 视图的时候,更新锁已经消失了。除非通过查询提示强制使用更新锁,可能永远无法从DBLocks 视图的输出结果或者对sys.dm_tran_locks 的直接观察中看到它们。
实例5:在可串行化隔离级别下使用索引进行更新
SQL 批处理
USE AdventureWorks;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRAN
UPDATE Production.Product
SET ListPrice = ListPrice * 0.6
WHERE Name LIKE 'Racing Socks%';
SELECT * FROM DBlocks
WHERE spid = @@spid
AND entity_name = 'Product';
COMMIT TRAN
DBlocks 的输出结果
spid dbname entity_name index_id resource description mode status
---- -------------- ----------- ---------- --------- -------------- -------- ------
54 AdventureWorks Product NULL OBJECT IX GRANT
54 AdventureWorks Product 1 PAGE 1:16897 IX GRANT
54 AdventureWorks Product 1 KEY (6a00dd896688) X GRANT
54 AdventureWorks Product 1 KEY (6b00b8eeda30) X GRANT
54 AdventureWorks Product 3 KEY (9502d56a217e) RangeS-U GRANT
54 AdventureWorks Product 3 PAGE 1:1767 IU GRANT
54 AdventureWorks Product 3 KEY (23027a50f6db) RangeS-U GRANT
54 AdventureWorks Product 3 KEY (9602945b3a67) RangeS-U GRANT
再次,可以注意到键范围锁存在于用来寻找相关记录的非聚集索引上。范围间隔本身只需要一个共享锁来防止插入,但是被搜索的键上拥有U锁,而其他进程都无法尝试更新。当实际的修改发生时,表自身(IndId = 1)的键上会获取排他锁。
下面看一个相同隔离级别下不使用索引来搜索的更新操作。
实例6:在可串行化隔离级别下不使用索引进行更新
SQL 批处理
USE AdventureWorks;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRAN
UPDATE Production.Product
SET ListPrice = ListPrice * 0.6
WHERE Color = 'White';
SELECT * FROM DBlocks
WHERE spid = @@spid
AND entity_name = 'Product';
COMMIT TRAN
DBlocks 的输出结果 (省略版)
spid dbname entity_name index_id resource description mode status
---- -------------- ----------- -------- -------- -------------- -------- ------
54 AdventureWorks Product NULL OBJECT IX GRANT
54 AdventureWorks Product 1 KEY (7900ac71caca) RangeS-U GRANT
54 AdventureWorks Product 1 KEY (6100dc0e675f) RangeS-U GRANT
54 AdventureWorks Product 1 KEY (5700a1a9278a) RangeS-U GRANT
54 AdventureWorks Product 1 PAGE 1:16898 IU GRANT
54 AdventureWorks Product 1 PAGE 1:16899 IU GRANT
54 AdventureWorks Product 1 PAGE 1:16896 IU GRANT
54 AdventureWorks Product 1 PAGE 1:16897 IX GRANT
54 AdventureWorks Product 1 PAGE 1:16900 IU GRANT
54 AdventureWorks Product 1 PAGE 1:16901 IU GRANT
54 AdventureWorks Product 1 KEY (5600c4ce9b32) RangeS-U GRANT
54 AdventureWorks Product 1 KEY (7300c89177a5) RangeS-U GRANT
54 AdventureWorks Product 1 KEY (7f00702ea1ef) RangeS-U GRANT
54 AdventureWorks Product 1 KEY (6b00b8eeda30) RangeX-X GRANT
54 AdventureWorks Product 1 KEY (c500b9eaac9c) RangeX-X GRANT
54 AdventureWorks Product 1 KEY (c6005745198e) RangeX-X GRANT
54 AdventureWorks Product 1 KEY (6a00dd896688) RangeX-X GRANT
这里的锁与上一个例子中的那些锁相似,除了全部的锁都存在于表本身(IndId = 1)。SQL Server 必须执行一次聚集索引扫描(在整张表上),因此所有键在最初取得了RangeS-U 锁,而最后当有四条记录被修改时,那些键上的锁就升级成RangeX-X 锁了。从上面的结果中可以看到全部的RangeX-X 锁,但并没有列出所有的RangeS-U锁,因为表中共有504 条记录。
实例7:新建表
SQL 批处理
USE AdventureWorks;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN TRAN
SELECT *
INTO newProducts
FROM Production.Product
WHERE ListPrice between 1 and 10;
SELECT * FROM DBlocks
WHERE spid = @@spid;
COMMIT TRAN
DBlocks 的输出结果 (省略版)
spid dbname entity_name index_id resource description mode status
---- -------------- ------------ -------- -------- ------------------ -------- -------
54 AdventureWorks n/a NULL DATABASE NULL GRANT
54 AdventureWorks n/a NULL DATABASE NULL GRANT
54 AdventureWorks n/a NULL DATABASE S GRANT
54 AdventureWorks n/a NULL METADATA user_type_id = 258 Sch-S GRANT
54 AdventureWorks n/a NULL METADATA data_space_id = 1 Sch-S GRANT
54 AdventureWorks n/a NULL DATABASE S GRANT
54 AdventureWorks n/a NULL METADATA $seq_type = 0,objec Sch-M GRANT
54 AdventureWorks n/a NULL METADATA user_type_id = 260 Sch-S GRANT
54 AdventureWorks sysrowsetcol NULL OBJECT IX GRANT
spid dbname entity_name index_id resource description mode status
---- -------------- ------------ -------- -------- ------------------ -------- -------
54 AdventureWorks sysrowsets NULL OBJECT IX GRANT
54 AdventureWorks sysallocunit NULL OBJECT IX GRANT
54 AdventureWorks syshobtcolum NULL OBJECT IX GRANT
54 AdventureWorks syshobts NULL OBJECT IX GRANT
54 AdventureWorks sysserefs NULL OBJECT IX GRANT
54 AdventureWorks sysschobjs NULL OBJECT IX GRANT
54 AdventureWorks syscolpars NULL OBJECT IX GRANT
54 AdventureWorks sysidxstats NULL OBJECT IX GRANT
54 AdventureWorks sysrowsetcol 1 KEY (15004f6b3486) X GRANT
54 AdventureWorks sysrowsetcol 1 KEY (0a00862c4e8e) X GRANT
54 AdventureWorks sysrowsets 1 KEY (000000aaec7b) X GRANT
54 AdventureWorks sysallocunit 1 KEY (00001f2dcf47) X GRANT
54 AdventureWorks syshobtcolum 1 KEY (1900f7d4e2cc) X GRANT
54 AdventureWorks syshobts 1 KEY (000000aaec7b) X GRANT
54 AdventureWorks NULL NULL RID 1:6707:1 X GRANT
54 AdventureWorks DBlocks NULL OBJECT IS GRANT
54 AdventureWorks newProducts NULL OBJECT Sch-M GRANT
54 AdventureWorks sysserefs 1 KEY (010025fabf73) X GRANT
54 AdventureWorks sysschobjs 1 KEY (3b0042322c99) X GRANT
54 AdventureWorks syscolpars 1 KEY (4200c1eb801c) X GRANT
54 AdventureWorks syscolpars 1 KEY (4e00092bfbc3) X GRANT
54 AdventureWorks sysidxstats 1 KEY (3b0006e110a6) X GRANT
54 AdventureWorks sysschobjs 2 KEY (9202706f3e6c) X GRANT
54 AdventureWorks syscolpars 2 KEY (6c0151be80af) X GRANT
54 AdventureWorks syscolpars 2 KEY (2c03557a0b9d) X GRANT
54 AdventureWorks sysidxstats 2 KEY (3c00f3332a43) X GRANT
54 AdventureWorks sysschobjs 3 KEY (9202d42ddd4d) X GRANT
54 AdventureWorks sysschobjs 4 KEY (3c0040d00163) X GRANT
54 AdventureWorks newProducts 0 PAGE 1:6707 X GRANT
54 AdventureWorks newProducts 0 HOBT Sch-M GRANT
实际上在这些锁中只有很少一些被newProducts 表的成分所占有。从entity_name 字段可以看到绝大多数的对象都是未记载的,而且一般都是不可见的系统表名。当创建新的表时,SQL Server 会在九个不同的系统表上获取锁以记录关于这个新表的信息。还可以注意到新表上存在架构修改(Sch-M)锁和其他的元数据锁。
最后一个例子将观察表上没有聚集索引时进行数据行更新操作的锁占有情况。
实例8:行级锁
SQL 批处理
USE AdventureWorks;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
BEGIN TRAN
UPDATE newProducts
SET ListPrice = 5.99
WHERE name = 'Road Bottle Cage';
SELECT * FROM DBlocks
WHERE spid = @@spid
AND entity_name = 'newProducts';
COMMIT TRAN
DBlocks 的输出结果
spid dbname entity_name index_id resource description mode status
---- -------------- ----------- ---------- --------- -------------- ------ ------
54 AdventureWorks newProducts NULL OBJECT IX GRANT
54 AdventureWorks newProducts 0 PAGE 1:6708 IX GRANT
54 AdventureWorks newProducts 0 RID 1:6708:5 X GRANT
newProducts 表上没有索引,因而实际符合搜索标准的行(RID)上存在的是一个排他(X)锁。对于RID 锁,description 字段实际上是以“文件号:页号:槽号”的形式来表示特定的行的。正如预料中的那样,分页和表上持有IX 锁。




