http://msdn.microsoft.com/zh-cn/library/ms184286.aspx
锁升级是将许多较细粒度的锁转换成数量更少的较粗粒度的锁的过程,这样可以减少系统开销,但却增加了并发争用的可能性。
当 SQL Server 数据库引擎获取低级别的锁时,它还将在包含更低级别对象的对象上放置意向锁:
- 当锁定行或索引键范围时,数据库引擎将在包含这些行或键的页上放置意向锁。
- 当锁定页时,数据库引擎将在包含这些页的更高级别的对象上放置意向锁。除了对象上的意向锁以外,以下对象上还需要意向页锁:
- 非聚集索引的叶级页
- 聚集索引的数据页
- 堆数据页
数据库引擎可以为同一语句执行行锁定和页锁定,以最大程度地减少锁的数量,并降低需要进行锁升级的可能性。例如,数据库引擎可以在非聚集索引上放置页锁(如果在索引节点中选择了足够的连续键来满足查询),而在数据上放置行锁。
升级锁时,数据库引擎将尝试将表上的意向锁改为对应的全锁,例如,将意向排他锁(IX 锁)改为排他锁(X 锁),或将意向共享锁(IS 锁)改为共享锁(S 锁)。如果锁升级尝试成功并获取全表锁,将释放事务在堆或索引上所持有的所有堆或 B 树锁、页锁(PAGE 锁)或行级锁(RID 锁)。如果无法获取全锁,当时不会发生锁升级,而数据库引擎将继续获取行、键或页锁。
数据库引擎不会将行锁或键范围锁升级到页锁,而是将它们直接升级到表锁。同样,页锁始终升级到表锁。在 SQL Server 2008 中,对于关联的分区,已分区表的锁定可以升级到 HoBT 级别,而不是表锁。HoBT 级锁不一定会锁定该分区的对齐 HoBT。
![]() |
---|
HoBT 级锁通常会增加并发情况,但是当锁定不同分区的每个事务都希望将其排他锁扩展到其他分区时,有可能会发生死锁。在极少数情况下,TABLE 锁定粒度可能更适合。
|
如果由于并发事务所持有的锁冲突而导致锁升级尝试失败,则数据库引擎将对事务获取的其他 1,250 个锁重试锁升级。
每个升级事件主要在单个 Transact-SQL 语句级别上操作。当事件启动时,只要活动语句满足升级阈值的要求,数据库引擎就会尝试升级当前事务在活动语句所引用的任何表中持有的所有锁。如果升级事件在语句访问表之前启动,则不会尝试升级该表上的锁。如果锁升级成功,只要表被当前语句引用并且包括在升级事件中,上一个语句中事务获取的、在事件启动时仍被持有的锁都将被升级。
例如,假定某个会话执行下列操作:
- 开始一个事务。
- 更新 TableA。这将在 TableA 中生成排他行锁,直到事务完成后才会释放该锁。
- 更新 TableB。这将在 TableB 中生成排他行锁,直到事务完成后才会释放该锁。
- 执行联接 TableA 和 TableC 的 SELECT 语句。查询执行计划要求先从 TableA 中检索行,然后才从 TableC 中检索的行。
- SELECT 语句在从 TableA 中检索行时(此时还没有访问 TableC)触发锁升级。
如果锁升级成功,只有会话在 TableA 中持有的锁才会升级。这包括来自 SELECT 语句的共享锁和来自上一个 UPDATE 语句的排他锁。由于决定是否应进行锁升级时只考虑会话在 TableA 上为 SELECT 语句获取的锁,所以一旦升级成功,会话在 TableA 上持有的所有锁都将被升级到该表上的排他锁,而 TableA 上的所有其他较低粒度的锁(包括意向锁)都将被释放。
不会尝试升级 TableB 上的锁,因为 SELECT 语句中没有 TableB 的活动引用。同样,也不会尝试升级 TableC 上尚未升级的锁,因为发生升级时尚未访问过该表。
![](http://i.msdn.microsoft.com/Global/Images/clear.gif)
如果没有使用 ALTER TABLE SET LOCK_ESCALATION 选项来禁用表的锁升级并且满足以下任一条件时,则将触发锁升级:
- 单个 Transact-SQL 语句在单个无分区表或索引上获得至少 5,000 个锁。
- 单个 Transact-SQL 语句在已分区表的单个分区上获得至少 5,000 个锁,并且 ALTER TABLE SET LOCK_ESCALATION 选项设为 AUTO。
- 数据库引擎实例中的锁的数量超出了内存或配置阈值。
如果由于锁冲突导致无法升级锁,则数据库引擎每当获取 1,250 个新锁时便会触发锁升级。
Transact-SQL 语句的升级阈值
当 Transact-SQL 语句在单个表或索引的引用上获取至少 5,000 个锁时,或在表已分区的情况下,在单个表分区或索引分区的引用上获取至少 5,000 个锁时,会触发锁升级。例如,如果该语句在一个索引上获取 3,000 个锁,在同一表中的另一个索引上获取 3,000 个锁,这种情况下不会触发锁升级。同样,如果语句中含有表的自联接,并且表的每一个引用仅在表中获取 3,000 个锁,则不会触发锁升级。
只有触发升级时已经访问的表才会发生锁升级。假定某个 SELECT 语句是一个按 TableA、TableB 和 TableC 的顺序访问三个表的联接。该语句在 TableA 的聚集索引中获取 3,000 个行锁,在 TableB 的聚集索引中获取至少 5,000 个行锁,但是仍无法访问 TableC。当数据库引擎检测到该语句在 TableB 中获取至少 5,000 个行锁时,会尝试升级当前事务在 TableB 中持有的所有锁。它还会尝试升级当前事务在 TableA 中持有的所有锁,但是由于 TableA 中锁的数量 < 5000,因此,升级无法成功。但它不会尝试在 TableC 中进行锁升级,因为发生升级时尚未访问该表。
数据库引擎实例的升级阈值
每当锁的数量大于锁升级的内存阈值时,数据库引擎都会触发锁升级。内存阈值取决于 locks 配置选项的设置:
- 如果 locks 选项设置为默认设置 0,当锁对象使用的内存是数据库引擎使用的内存的 40%(不包括 AWE 内存)时,将达到锁升级阈值。用于表示锁的数据结构大约有 100 个字节长。该阈值是动态的,因为 数据库引擎动态地获得和释放内存来针对变化的工作负荷进行调整。
- 如果 locks 选项设置为非 0 值,则锁升级阈值是 locks 选项的值的 40%(或者更低,如果存在内存不足的压力)。
数据库引擎可以为升级选择任何会话中的活动语句,而且,只要实例中使用的锁内存保持在阈值之上,每获取 1,250 个新锁,它就会为升级选择语句。
![](http://i.msdn.microsoft.com/Global/Images/clear.gif)
发生锁升级时,为堆或索引选择的锁必须足够强,才能满足限制性最强的较低级别的锁的要求。
例如,假定会话执行下列操作:
- 开始一个事务。
- 更新包含聚集索引的表。
- 发出引用同一个表的 SELECT 语句。
UPDATE 语句将获取下列锁:
- 已更新数据行上的排他锁(X 锁)。
- 包含那些行的聚集索引页上的意向排他锁(IX 锁)。
- 聚集索引上的 IX 锁和表上的 IX 锁。
SELECT 语句将获取下列锁:
- 所读取的所有数据行上的共享锁(S 锁),除非行已被来自 UPDATE 语句的 X 锁保护。
- 包含那些行的所有聚集索引页上的意向共享锁,除非页已被 IX 锁保护。
- 在聚集索引或表上不会获取锁,因为它们已被 IX 锁保护。
如果 SELECT 获取了触发锁升级的足够锁并且升级成功,表上的 IX 锁将被转换为 X 锁,而所有行、页和索引锁都将被释放。更新和读取操作都受表上的 X 锁保护。
![](http://i.msdn.microsoft.com/Global/Images/clear.gif)
在大多数情况下,数据库引擎使用默认的锁定和锁升级设置进行操作时提供的性能最佳。如果数据库引擎实例生成大量锁并且频繁进行锁升级,请考虑通过下列方法减少锁定:
- 对于读取操作,使用不会生成共享锁的隔离级别。
- 当 READ_COMMITTED_SNAPSHOT 数据库选项为 ON 时,使用 READ COMMITTED 隔离级别。
- 使用 SNAPSHOT 隔离级别。
- 使用 READ UNCOMMITTED 隔离级别。此隔离级别只能用于能对脏读进行操作的系统。
![]() |
---|
更改隔离级别会影响数据库引擎实例上的所有表。
|
- 使用 PAGLOCK 或 TABLOCK 表提示,使数据库引擎使用页、堆或索引锁而不是行锁。但是,使用此选项增加了用户阻止其他用户尝试访问相同数据的问题,对于并发用户较多的系统,不应使用此选项。
- 对于已分区表,使用 ALTER TABLE 的 LOCK_ESCALATION 选项将锁升级到 HoBT 级而不是表级,或者禁用锁升级。
还可以使用跟踪标志 1211 和 1224 来禁用所有或某些锁升级。有关详细信息,请参阅跟踪标志 (Transact-SQL)。此外,还可以使用 SQL Server Profiler Lock:Escalation 事件监视锁升级,请参阅使用 SQL Server Profiler。
http://msdn.microsoft.com/zh-cn/library/ms190273.aspx
-
SET
( LOCK_ESCALATION = { AUTO | TABLE | DISABLE }
)
-
指定允许的对表的锁进行升级的方法。
-
AUTO
-
此选项允许 SQL Server 数据库引擎选择适合于表架构的锁升级粒度。
- 如果该表已分区,则允许将锁升级到分区。锁升级到分区级别之后,该锁以后将不会升级到 TABLE 粒度。
- 如果该表未分区,则会将锁升级到 TABLE 粒度。
-
TABLE
-
无论表是否已分区,都会在表级粒度完成锁升级。此行为与 SQL Server 2005 中相同。默认值为 TABLE。
-
DISABLE
-
在大多数情况下禁止锁升级。表级别的锁未完全禁止。例如,当扫描在可序列化隔离级别下没有聚集索引的表时,数据库引擎必须使用表锁来保证数据的完整性。
-
-
REBUILD
-
使用 REBUILD WITH 语法可重新生成包含分区表中的所有分区的整个表。如果表具有聚集索引,则 REBUILD 选项将重新生成该聚集索引。REBUILD 可作为 ONLINE 操作执行。
使用 REBUILD PARTITION 语法可重新生成分区表中的单个分区。
-
PARTITION = ALL
-
更改分区压缩设置时重新生成所有分区。
-
REBUILD WITH ( <rebuild_option> )
-
为具有聚集索引的表应用所有选项。如果表没有聚集索引,则只有部分选项会影响堆结构。
有关重新生成选项的完整说明,请参阅 index_option (Transact-SQL)。
-
DATA_COMPRESSION
-
为指定的表、分区号或分区范围指定数据压缩选项。选项如下所示:
-
NONE
-
不压缩表或指定的分区。
-
ROW
-
使用行压缩来压缩表或指定的分区。
-
PAGE
-
使用页压缩来压缩表或指定的分区。
若要同时重新生成多个分区,请参阅 index_option (Transact-SQL)。如果表没有聚集索引,则更改数据压缩会重新生成堆和非聚集索引。有关压缩的详细信息,请参阅创建压缩表和索引。
-
Q. 允许已分区表中的锁升级
下面的示例在已分区表的分区级别启用锁升级。如果该表未分区,则会将锁升级到 TABLE 级别。
ALTER TABLE T1 SET (LOCK_ESCALATION = AUTO) GO
http://msdn.microsoft.com/zh-cn/library/ms187504.aspx
对于大型计算机系统,在经常被引用的对象上放置的锁可能会变成性能瓶颈,因为获取和释放锁对内部锁资源造成了争用。锁分区通过将单个锁资源拆分为多个锁资源而提高了锁性能。此功能只适用于拥有 16 个或更多 CPU 的系统,它是自动启用的,而且无法禁用。只有对象锁可以分区。
![]() |
---|
拥有子类型的对象锁不能分区。有关详细信息,请参阅 sys.dm_tran_locks (Transact-SQL)。
|
![](http://i.msdn.microsoft.com/Global/Images/clear.gif)
![](http://i.msdn.microsoft.com/Global/Images/clear.gif)
默认情况下,对于具有 16 个或更多 CPU 的系统,锁分区是打开的。启用锁分区后,将在 SQL Server 错误日志中记录一条信息性消息。
获取已分区资源的锁时:
- 只能获取单个分区的 NL、SCH-S、IS、IU 和 IX 锁模式。
- 对于以分区 ID 0 开始并且按照分区 ID 顺序排列的所有分区,必须获取非 NL、SCH-S、IS、IU 和 IX 模式的共享锁 (S)、排他锁 (X) 和其他锁。已分区资源的这些锁将比相同模式中未分区资源的锁占用更多的内存,因为每个分区都是一个有效的单独锁。内存的增加由分区数决定。Windows 性能监视器中 SQL Server 锁计数器将显示已分区和未分区锁所使用内存信息。
启动一个事务时,它将被分配给一个分区。对于此事务,可以分区的所有锁请求都使用分配给该事务的分区。按照此方法,不同事务对相同对象的锁资源的访问被分布到不同的分区中。
sys.dm_tran_locks 动态管理视图中的 resource_lock_partition 列为锁分区资源提供锁分区 ID。有关详细信息,请参阅 sys.dm_tran_locks (Transact-SQL)。
在 SQL Server Profiler中的 Locks 事件下,BigintData1 列为每个锁分区资源提供锁分区 ID。
![](http://i.msdn.microsoft.com/Global/Images/clear.gif)
以下代码示例说明了锁分区。在这些示例中,为了显示一个具有 16 个 CPU 的计算机系统上的锁分区行为,在两个不同的会话中执行了两个事务。
这些 Transact-SQL 语句创建了后续示例中使用的测试对象。
USE AdventureWorks; GO -- Create a test table. CREATE TABLE TestTable (col1 int); GO -- Create a clustered index on the table. CREATE CLUSTERED INDEX ci_TestTable ON TestTable (col1); GO -- Populate the table. INSERT INTO TestTable VALUES (1); GO
示例 A
会话 1:
在一个事务中执行 SELECT 语句。由于 HOLDLOCK
锁提示的原因,此语句将获取并保留一个对此表的意向共享锁(IS 锁)(此例中忽略行锁和页锁)。IS 锁只能在分配给事务的分区中获取。对于此示例,假定 IS 锁是在 ID 为 7 的分区中获取的。
-- Start a transaction. BEGIN TRANSACTION -- This SELECT statement will acquire an IS lock on the table. SELECT col1 FROM TestTable WITH (HOLDLOCK);
会话 2:
启动事务,在此事务下运行 SELECT 语句将获取共享锁(S 锁)并将其保留在表中。将获取所有分区的 S 锁,这将产生多个表锁,每个分区一个。例如,在具有 16 个 CPU 的系统上,将在锁分区 ID 0-15 中发出 16 个 S 锁。因为 S 锁与分区 ID 7 上由会话 1 中的事务持有的 IS 锁兼容,所以事务之间没有阻塞。
BEGIN TRANSACTION SELECT col1 FROM TestTable WITH (TABLOCK, HOLDLOCK);
会话 1:
将在会话 1 下仍然活动的事务下执行以下 SELECT 语句。由于排他 (X) 表锁提示,事务将尝试获取表的 X 锁。但是,由会话 2 中的事务持有的 S 锁将阻塞分区 ID 0 的 X 锁。
SELECT col1 FROM TestTable WITH (TABLOCKX);
示例 B
会话 1:
在一个事务中执行 SELECT 语句。由于 HOLDLOCK
锁提示的原因,此语句将获取并保留一个对此表的意向共享锁(IS 锁)(此例中忽略行锁和页锁)。IS 锁只能在分配给事务的分区中获取。对于此示例,假定 IS 锁是在 ID 为 6 的分区中获取的。
-- Start a transaction. BEGIN TRANSACTION -- This SELECT statement will acquire an IS lock on the table. SELECT col1 FROM TestTable WITH (HOLDLOCK);
会话 2:
在一个事务中执行 SELECT 语句。由于 TABLOCKX
锁提示,事务将尝试获取表的排他锁(X 锁)。请记住,必须获取从分区 ID 0 开始的所有分区的 X 锁。将获取分区 ID 0-5 的 X 锁,但它会被为分区 ID 6 获取的 IS 锁阻塞。
对于尚未获取 X 锁的分区 ID 7-15,其他事务可以继续获取锁。
BEGIN TRANSACTION SELECT col1 FROM TestTable WITH (TABLOCKX, HOLDLOCK);
![](http://i.msdn.microsoft.com/Global/Images/clear.gif)