锁升级(数据库引擎)

锁升级是将许多较细粒度的锁转换成数量更少的较粗粒度的锁的过程,这样可以减少系统开销,但却增加了并发争用的可能性。

当 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 中生成排他行锁,直到事务完成后才会释放该锁。

  • 执行联接 TableATableC 的 SELECT 语句。 查询执行计划要求先从 TableA 中检索行,然后才从 TableC 中检索的行。

  • SELECT 语句在从 TableA 中检索行时(此时还没有访问 TableC)触发锁升级。

如果锁升级成功,只有会话在 TableA 中持有的锁才会升级。 这包括来自 SELECT 语句的共享锁和来自上一个 UPDATE 语句的排他锁。 由于决定是否应进行锁升级时只考虑会话在 TableA 上为 SELECT 语句获取的锁,所以一旦升级成功,会话在 TableA 上持有的所有锁都将被升级到该表上的排他锁,而 TableA 上的所有其他较低粒度的锁(包括意向锁)都将被释放。

不会尝试升级 TableB 上的锁,因为 SELECT 语句中没有 TableB 的活动引用。 同样,也不会尝试升级 TableC 上尚未升级的锁,因为发生升级时尚未访问过该表。

锁升级阈值

如果没有使用 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 语句是一个按 TableATableBTableC 的顺序访问三个表的联接。 该语句在 TableA 的聚集索引中获取 3,000 个行锁,在 TableB 的聚集索引中获取至少 5,000 个行锁,但是仍无法访问 TableC。 当数据库引擎检测到该语句在 TableB 中获取至少 5,000 个行锁时,会尝试升级当前事务在 TableB 中持有的所有锁。 它还会尝试升级当前事务在 TableA 中持有的所有锁,但是由于 TableA 中锁的数量 < 5000,因此,升级无法成功。 但它不会尝试在 TableC 中进行锁升级,因为发生升级时尚未访问该表。

数据库引擎实例的升级阈值

每当锁的数量大于锁升级的内存阈值时,数据库引擎都会触发锁升级。 内存阈值取决于 locks 配置选项的设置:

  • 如果 locks 选项设置为默认设置 0,当锁对象使用的内存是数据库引擎使用的内存的 24%(不包括 AWE 内存)时,将达到锁升级阈值。 用于表示锁的数据结构大约有 100 个字节长。 该阈值是动态的,因为 数据库引擎动态地获得和释放内存来针对变化的工作负荷进行调整。

  • 如果 locks 选项设置为非 0 值,则锁升级阈值是 locks 选项的值的 40%(或者更低,如果存在内存不足的压力)。

数据库引擎可以为升级选择任何会话中的活动语句,而且,只要实例中使用的锁内存保持在阈值之上,每获取 1,250 个新锁,它就会为升级选择语句。

升级混合锁类型

发生锁升级时,为堆或索引选择的锁必须足够强,才能满足限制性最强的较低级别的锁的要求。

例如,假定会话执行下列操作:

  • 开始一个事务。

  • 更新包含聚集索引的表。

  • 发出引用同一个表的 SELECT 语句。

UPDATE 语句将获取下列锁:

  • 已更新数据行上的排他锁(X 锁)。

  • 包含那些行的聚集索引页上的意向排他锁(IX 锁)。

  • 聚集索引上的 IX 锁和表上的 IX 锁。

SELECT 语句将获取下列锁:

  • 所读取的所有数据行上的共享锁(S 锁),除非行已被来自 UPDATE 语句的 X 锁保护。

  • 包含那些行的所有聚集索引页上的意向共享锁,除非页已被 IX 锁保护。

  • 在聚集索引或表上不会获取锁,因为它们已被 IX 锁保护。

如果 SELECT 获取了触发锁升级的足够锁并且升级成功,表上的 IX 锁将被转换为 X 锁,而所有行、页和索引锁都将被释放。 更新和读取操作都受表上的 X 锁保护。

减少锁定和升级

在大多数情况下,数据库引擎使用默认的锁定和锁升级设置进行操作时提供的性能最佳。 如果数据库引擎实例生成大量锁并且频繁进行锁升级,请考虑通过下列方法减少锁定:

  • 对于读取操作,使用不会生成共享锁的隔离级别。

    • 当 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