为什么并行会导致锁升级,临界点在哪里?

1 sql-server parallelism lock-escalation

我使用定制的 Stack Overflow 数据库(180GB)并运行一个简单的更新查询:(Users 表上只有一个聚集索引)

Begin Tran
Update U set U.Reputation=100000 
from StackOverflow.dbo.Users as U 
where U.CreationDate = '2008-10-10 14:26:33.540'
Run Code Online (Sandbox Code Playgroud)

查询计划

在此输入图像描述

此查询会导致锁升级。我无法在另一个窗口中使用同一个表运行查询:

select * from StackOverflow.dbo.Users as U where U.id=11
Run Code Online (Sandbox Code Playgroud)

如果我option (maxdop 1)在查询末尾添加以避免并行,则一切都很好(计划)。

在较小的 Stack Overflow DB (StackOverflow2013 - 52GB) 中不会发生锁升级(计划)。

如何确定导致升级的数据量?

我使用 SQL Server 2019。数据库兼容级别为 150。

表信息:

  • StackOverflow2013.dbo.Users -- 2 465 713 行;45 184 页
  • StackOverflow.dbo.Users -- 8 917 507 行;143 667 页

Pau*_*ite 5

其中涉及到机会的因素。

\n

我可以使用位于 的 Stack Overflow 2013 示例数据库可靠地重现您场景中的锁升级MAXDOP 8MAXDOP 12在(我可以在此实例上使用的最多)或在 处没有锁升级MAXDOP 1

\n

MAXDOP 1在测试之前,我使用100重建了 Users 表FILLFACTOR,以确保页面尽可能满:

\n
ALTER TABLE dbo.Users \nREBUILD \nWITH \n(\n    FILLFACTOR = 100, \n    MAXDOP = 1, \n    ALLOW_ROW_LOCKS = ON,\n    ALLOW_PAGE_LOCKS = ON,\n    DATA_COMPRESSION = NONE,\n    ONLINE = OFF\n);\n
Run Code Online (Sandbox Code Playgroud)\n

如果没有锁定粒度提示,SQL Server 存储引擎会选择使用页级锁定来处理聚集索引扫描。对于大表的扫描来说这是很正常的。获取和释放行锁会增加太多开销,并且表锁不利于并发性。

\n

串行计划中不会发生锁升级,因为一旦针对谓词测试了该页上的所有行并发现不匹配,SQL Server 就可以释放该页上的锁。这是可能的,因为执行计划不包含任何阻塞运算符。

\n

并行计划有所不同,因为Gather Streams运算符部分阻塞。在Update操作符处理来自该数据包的行之前,可以从扫描中生成多于一行并保存在交换缓冲区中。

\n

为了保证并行计划的正确性,SQL Server 必须能够在必要时一次在多个页面上持有锁。这就需要使用锁类,如果需要的话,它允许在语句结束时保持多个锁。

\n

该引擎确实包含在安全时尽早释放锁类中持有的锁的优化,但细节很复杂并且没有记录。在并行执行计划中,锁保留到语句末尾。

\n

更准确地说,页面级更新锁是在扫描操作员处获取的。这些分布在并行线程中,每个线程根据当前持有的锁数量独立尝试升级。

\n

在我的测试中MAXDOP 8,一两个线程最终将其页级更新锁升级为独占表锁(表级更新锁不存在)。在 时MAXDOP 12,工作通常分布得足够均匀,以至于没有线程获取足够的锁来尝试锁升级。所有四个线程都MAXDOP 4升级了它们的锁。

\n

注意:一个或多个线程可能会升级到表锁,而同一计划中的其他线程会继续以不同的粒度持有同一对象上的锁,因为这些线程不会触发升级。

\n
\n

我没有 180GB Stack Overflow 数据库,也不愿意下载它只是为了测试,但随着页面数量的增加,线程获取足够页面锁以尝试升级的机会明显增加。在 DOP 足够高且分布均匀的情况下,仍然可以避免升级。这就是我之前提到的机会因素。

\n

我用来监视锁升级“尝试”和成功的脚本如下所示:

\n
SELECT \n    IOS.row_lock_count, \n    IOS.page_lock_count, \n    IOS.index_lock_promotion_attempt_count, \n    IOS.index_lock_promotion_count\nFROM sys.dm_db_index_operational_stats\n(\n    DB_ID(), OBJECT_ID(N\'dbo.Users\', \'U\'), NULL, NULL\n) AS IOS;\n\nBEGIN TRANSACTION;\n\n    UPDATE U \n    SET U.Reputation = 100000 \n    FROM dbo.Users AS U\n    WHERE \n        U.CreationDate = \'2008-10-10 14:26:33.540\'\n    OPTION (MAXDOP 8, RECOMPILE);\n\n    SELECT \n        IOS.row_lock_count, \n        IOS.page_lock_count, \n        IOS.index_lock_promotion_attempt_count, \n        IOS.index_lock_promotion_count\n    FROM sys.dm_db_index_operational_stats\n    (\n        DB_ID(), OBJECT_ID(N\'dbo.Users\', \'U\'), NULL, NULL\n    ) AS IOS;\n\nROLLBACK TRANSACTION; \n
Run Code Online (Sandbox Code Playgroud)\n

并行工作线程之一升级的运行的示例输出:

\n

一个线程升级了它的锁

\n

我还使用lock_escalation扩展事件来确认升级:

\n

扩展事件输出

\n

有关锁升级内部原理的更多详细信息,请参阅我的系列文章:

\n\n

Microsoft还提出了解决 SQL Server 中锁升级引起的阻塞问题,但在许多方面并不精确,而且总体上也不完整。

\n