SQL Server 锁升级

锁升级(Lock Escalations)是 SQL Server 使用的优化技术,用来控制在 SQL Server 锁管理里把持锁的数量。锁升级是将许多细粒度锁 ((如行或页) 锁)转换为表锁的过程。

Microsoft SQL Server 动态确定何时执行锁定升级。 做出此决定时,SQL Server 会考虑在特定扫描中保留的锁定数、整个事务持有的锁数以及要用于整个系统中的锁定的内存。 通常情况下,SQL Server 的默认行为会导致锁定升级仅发生在将提高性能的那些点处,或者必须将过多的系统锁定内存减少为更合理的级别时。 但是,某些应用程序或查询设计可能会在不需要时触发锁定升级,升级后的表锁定可能会阻止其他用户。

本文将介绍 SQL Server 锁升级的机制,先从锁层级(Lock Hierarchy )开始,因为这是在像 SQL Server 这类关系数据库里,为什么有锁升级概念存在的原因。

锁层级(Lock Hierarchy )

SQL Server 的锁层级:
在这里插入图片描述

从上图可以看到,锁层级开始于数据库层级,向下至行层级。在数据库本身层级,一直有一个共享锁(Shared Lock (S) )。当有查询连接到一个数据库(例如USE MyDatabase),共享锁会阻止数据库删除,或者在那个数据库上还原备份。

当进行一个操作时,在数据库层级下,在表上、页上、记录上都会有锁。

当执行一个 SELECT 语句时,在表和页上会有一个意向共享锁(Intent Shared Lock (IS) ),在记录本身上有共享锁(Shared Lock (S) )。

当进行数据修改语句(INSERT,UPDATE,DELETE)时,在表和页上会有一个意向排它或更新锁( Intent Exclusive or Update Lock (IX or IU) ),在改变的记录上有排它或更新锁(Exclusive or Update Lock (X or U) )。当多个线程尝试在锁层级里并发获取锁时,SQL Server 会一直获取从头到脚的锁来阻止所谓的竞争危害。

现通过一个示例讲解锁升级的必要性。假设对 1 张表中的 20000 条记录进行删除操作,想象下这个锁层级会是什么样的?我们假定记录是 400 bytes 长,这就意味这在 8kb 的页里刚好有 20 条记录,20000条记录散布在 1000 个页上。因此,锁层级如下图所示:
在这里插入图片描述

  • 数据库上有 1 个共享锁(S);
  • 表上有 1 个意向排它锁(IX);
  • 页上有 1000 个意向排它锁(IX);
  • 记录本身有 20000 个排它锁(X)。

对于 DELETE 操作总计获取了 21002 个锁。在 SQL Server 里每个锁需要 96 bytes 的内存,因此对这个简单的查询需要 1.9MB 的锁。当并行执行多个查询时,这个不会无限扩展。SQL Server 现在用所谓的锁升级(Lock Escalation)实现。

锁升级(Lock Escalations)

在锁层级里一旦有超过 5000 个锁,SQL Server 会升级这么多的精细粒度(fine-granularity)的锁为简单的粗粒度(coarse-granularity)的锁。默认情况下,SQL Server “总是”升级到表层级。这意味着锁层级从刚才例子的样子,在锁升级成功执行后,变成如下的样子:
在这里插入图片描述
如上图所示,在表本身上只有一个大锁。在 DELETE 操作的情况下,在表层级你有一个排它锁(X)。这会以负面伤及你数据库的并发。在表层级把持一个排它锁(X)意味者没有其他会话可以访问这个表 —— 每个其它查询会阻塞。

当在可重读隔离级别(Repeatable Read Isolation Level)运行 SELECT 语句时,也在把持共享锁(S)直到事务结束,这也就是说只要读超过 5000 条记录就会发生锁升级。这里的结果是一个共享锁(S)在表本身!表只是暂时只读,因为在表上每个其它数据修改都会阻塞!

这里还有个误解 —— SQL Server 锁升级会从行层级到页层级,最后到表层级。错了!在 SQL Server 里没有这样的代码路径存在!默认情况下,SQL Server 总是会直接升级到表层级。到页层级的升级策略不存在。如果表被分区了(只针对企业版本的 SQL Server),可以配置升级到分区层级。但建议必须非常仔细测试数据访问模式,因为锁升级到分区层级会引起死锁。因此这个选项默认是没启用的。

自 SQL Server 2008 开始,可以控制 SQL Server 如何进行锁升级——通过 ALTER TABLE 语句和 LOCK_ESCALTATION 属性。

ALTER TABLE SET LOCK_ESCALATION =

?处有 3 个可用选项:

  • TABLE
  • AUTO
  • DISABLE

默认选项是 TABLE,意味着 SQL Server 总是执行锁升级到表层级——即使这个表已被分区。

如果表已被分区,想设置分区层级的锁升级(如果已经测试了数据访问模式,用它不会引起死锁),那么可以修改选项为 AUTO 。AUTO 意味着你的锁升级会执行到分区层级。

DISABLE 选项表示完全禁用表的锁升级。但是禁用锁升级并不是最好的选项,因为 SQL Server 的锁管理器会消耗大量的内存。

5000阈值:

  • 如果没有使用 ALTER TABLE SET LOCK_ESCALATION 选项来禁用表的锁升级并且满足以下任一条件时,则将触发锁升级:
  • 单个 Transact-SQL 语句在单个无分区表或索引上获得至少 5,000 个锁。
  • 单个 Transact-SQL 语句在已分区表的单个分区上获得至少 5,000 个锁,并且 ALTER TABLE SET LOCK_ESCALATION 选项设为 AUTO。
  • 数据库引擎实例中的锁的数量超出了内存或配置阈值。
  • 如果由于锁冲突导致无法升级锁,则数据库引擎每当获取 1,250 个新锁时便会触发锁升级。

锁升级导致的阻止问题

虽然锁升级会带来一定效率的提供,但它同时也存在一个不能忽略的问题:锁粒度的降低会导致并发的性能的降低。
而且,某些应用程序或查询设计可能会在不需要时触发锁升级,升级后的表锁定可能会阻止其他用户。 下面讨论如何确定锁升级是否导致阻止,以及如何处理不需要的锁升级。

确定锁升级是否导致阻止

锁升级不会导致大多数阻止问题。 若要确定在遇到阻止问题时是否发生锁升级,请启动包含事件的 SQL 事件探查器跟踪 Lock:Escalation 。 如果看不到任何 Lock:Escalation 事件,则服务器上未发生锁升级。

如果发生了锁升级,请验证升级的表锁定是否阻止其他用户。

若要详细了解如何识别 head 阻止程序,以及如何识别阻止其他服务器进程 Id (Spid) 的头拦截程序持有的锁定资源,请参阅 INF:了解和解决 SQL server 阻止问题

如果阻止其他用户的锁定不是选项卡( (表级) 锁的 (共享) 的锁定模式),或者是 X (独占) ,则不会出现锁升级问题。 特别是,如果 TAB 锁是意图锁 (例如,的锁定模式为、IU 或 IX) ,这并不是锁升级的结果。 如果锁定问题不是由锁升级导致的,请参阅 INF:了解和解决 SQL Server 阻止问题 故障排除步骤

阻止锁定升级

防止锁升级的最简单且最安全的方法是:将事务保持较短,并减小锁定占用率较高的查询,以便不超过锁升级阈值。 有几种方法可获得此目标,其中的许多方法均已列出:

1)将大型批处理操作分解为几个较小的操作。 例如,假设您已运行以下查询以从审核表中删除几百万个旧记录,然后您发现它导致锁定其他用户阻止的锁升级:

DELETE FROM LogMessages WHERE LogDate < '2/1/2002'

通过将这些记录一次删除几百,可以显著减少每个事务累积的锁数并防止锁升级。 例如:

SET ROWCOUNT 500
delete_more:
DELETE FROM LogMessages WHERE LogDate < '2/1/2002'
IF @@ROWCOUNT > 0 GOTO delete_more
SET ROWCOUNT 0

2)通过使查询尽可能高效,降低查询锁的占用。 大型扫描或大量书签查找可能会增加锁定升级的机会;此外,它还增加了死锁的可能性,并对并发和性能产生不利影响。 在找到导致锁定升级的查询后,请查找创建新索引的商机,或将列添加到现有索引以删除索引或表扫描,并最大限度地提高索引查找的效率。 请考虑将查询粘贴到查询分析器查询窗口中,以对其执行自动索引分析。 若要执行此操作,请在 “查询” 菜单上,单击 “SQL Server 2000 中的索引优化向导”,或单击 “在 SQL Server 7.0 中执行索引分析”。

此优化的一个目标是使索引查找尽可能少地返回行,以最大限度地减少书签查找的成本 (最大化查询) 的索引的选择性。 如果 SQL Server 估计书签查找逻辑运算符可能返回多行,则它可能会使用 PREFETCH 来执行书签查找。 如果 SQL Server 确实用于 PREFETCH 查找书签,则它必须将查询的一部分的事务隔离级别增加到查询的某一部分的可重复读取。 这意味着,对于已读的隔离级别,可能会在 SELECT 聚集索引和一个非聚集索引) 上获取许多 (的键锁,这会导致此类查询超过锁升级阈值。 如果您发现升级的锁定是共享表锁定,这一点尤为重要,但在默认的已读承诺隔离级别上通常不会看到。 如果书签查找 WITH PREFETCH 子句导致升级,请考虑将其他列添加到索引查找中显示的非聚集索引或查询计划中的书签查找逻辑运算符下方的索引扫描逻辑运算符。 可以创建覆盖索引 (索引包括在查询) 中使用的表中的所有列,或者至少是一个索引,该索引涵盖用于联接条件或 WHERE 子句中的列(如果包括选择列列表中的所有内容都不切实际)。

嵌套循环联接也可能使用 PREFETCH ,这将导致相同的锁定行为。

3)如果不同的 SPID 当前持有不兼容的表锁定,则无法进行锁定升级。 锁升级总是升级到表锁定,永远不会升级页面锁。 此外,如果由于另一个 SPID 保留不兼容的选项卡锁而导致锁定升级尝试失败,则尝试升级的查询不会在等待 TAB 锁的同时阻止。 相反,它会继续在其原始、更精细的级别 (行、键或页面) 中获取锁定,并定期进行额外的升级尝试。 因此,防止对特定表进行锁定升级的一种方法是,获取并在与提升的锁定类型不兼容的其他连接上保留锁定。 在表级别上的 IX (意向独占) 锁定不会锁定任何行或页面,但它仍然与已升级的 S (shared) 或 X (exclusive) TAB 锁不兼容。 例如,假设您必须运行一个批处理作业来修改 mytable 表中的多个行,并导致由于锁升级时发生阻止。 如果此作业总在不到一小时的时间内完成,则可以创建包含以下代码的 Transact-sql 作业,并将新作业安排在批处理作业的开始时间前数分钟内启动:

BEGIN TRAN
SELECT * FROM mytable (UPDLOCK, HOLDLOCK) WHERE 1=0
WAITFOR DELAY '1:00:00'
COMMIT TRAN

此查询获取并在 mytable 上为一个小时获取并保存一个 IX 锁,这样可以避免在该时间段内对表进行锁升级。 此批处理不会修改任何数据,也不会阻止其他查询 (除非其他查询强制表锁定和 TABLOCK 提示,或者管理员已使用存储过程) 禁用了页或行锁定 sp_indexoption 。

此外,还可以通过启用跟踪标记1211禁用锁升级。 但是,此跟踪标志将禁用 SQL Server 实例中全局的所有锁升级。 在 SQL Server 中,锁升级通过最大限度地降低因获取和释放数千个锁而降低的查询的效率,使其在 SQL Server 中成为一种非常有用的用途。 锁升级还有助于最大限度地减少所需的内存,以跟踪锁定。 SQL Server 可以为锁定结构动态分配的内存是有限的,因此,如果您禁用锁升级并且锁定内存变得足够大,则尝试为任何查询分配额外的锁可能会失败,并且会发生以下错误:

错误:1204,严重度:19,状态:1
现在 SQL Server 无法获取锁定资源。 当活动用户较少或要求系统管理员检查 SQL Server 锁定和内存配置时,请重新运行您的语句。

备注:
如果出现1204错误,它将停止对当前语句的处理,并导致活动事务的回滚。 如果重新启动 SQL Server 服务,回滚本身可能会阻止用户或导致长时间的数据库恢复时间。 您可以使用 SQL Server 配置管理器将此跟踪标志添加 ( T1211) 。
若要使新的启动参数生效,必须循环 SQL Server 服务。 如果 DBCC TRACEON (1211, -1) 在 SQL Server Management Studio 中运行查询,则 “查询分析器” 跟踪标志会立即生效。
但是,如果不添加-T1211 启动参数,则在重启 SQL Server 服务时,traceon 命令的效果将会丢失。 打开跟踪标志将阻止以后的任何锁升级,但不会撤消活动事务中已发生的任何定升级。

使用 lock 提示(如 ROWLOCK)仅更改初始锁定计划。 锁定提示不会阻止定升级。

参考:
https://www.cnblogs.com/kingwwz/p/5916951.html
https://docs.microsoft.com/zh-CN/troubleshoot/sql/performance/resolve-blocking-problems-caused-lock-escalation

猜你喜欢

转载自blog.csdn.net/Ruishine/article/details/114119836