SQL Server 独占 (X) 锁不会一直阻塞资源上的共享 (S) 锁

Ian*_*ork 5 sql-server locking

我对 SQL Server 中的锁定的理解是,如果一个进程在索引中的资源 ega 键行上持有排它 (X) 锁,则另一个进程无法获取同一资源上的共享 (S) 锁,必须等待它被发布。

我一直在尝试演示一个修复程序,用于解决访问同一个表的 2 个不同存储过程之间的死锁,一个使用显式事务来包装一个 SELECT 然后一个 DELETE,另一个只执行 SELECT 而没有显式事务. 这些都发生在 READ COMMITTED 事务隔离级别下。

我完成了在 2 个 SSMS 查询窗口中模拟每个过程的步骤,并查询 sys.dm_tran_locks 以查看每个步骤后持有和等待的锁。

我希望有人解释为什么,尽管有时我确实会遇到预期的死锁,但并非总是如此,而且我可以看到 X 锁已被授予一个连接,而我仍然可以从另一个连接中进行选择。

该演示是人为的,以及在非聚簇索引上拆分 SELECT 的原因,以获取聚簇键值,其中 SELECT 使用聚簇键从聚簇索引中读取其他列(并将两个选择包装在显式事务中HOLDLOCK 在第一个)是模拟我的真实世界查询的实际查询计划,它在非聚集索引上寻找并在聚集索引上执行键查找。我需要展示当读写连接上的 DELETE 查询与只读连接上的 SELECT 冲突时会发生什么

测试数据库不允许 SNAPSHOT ISOLATION 或 READ COMMITTED SNAPSHOT,并且两个连接都已显式设置为 READ COMMITTED 事务隔离级别,因此应该需要共享锁来读取行。

我已经在我的本地机器上使用 SQL2017 CU9(在 64 位 Windows 10 Enterprise build 16299 上)、在带有 SQL2017 CU7 的服务器上(在 Windows Server 2016 Standard build 14393 上)以及带有 SQL2016 sp1-CU2(在 Windows Server 上)的服务器上进行了测试2012 R2 标准版本 9600)。所有都是 64 位开发人员版本的 SQL Server。

我在下面包含了重现此内容的代码:-

创建数据库和表,然后填充它

CREATE DATABASE LockingRepro;
GO
USE [LockingRepro];
GO

CREATE TABLE dbo.Person (   PersonId            INT         NOT NULL    CONSTRAINT PK_Person PRIMARY KEY CLUSTERED,
                            LoginId             VARCHAR(50) NOT NULL    
                         )   ON [PRIMARY];  


CREATE TABLE dbo.PersonSession (    PersonId            INT                 NOT NULL CONSTRAINT PK_PersonSession PRIMARY KEY CLUSTERED,
                                    SessionId           UNIQUEIDENTIFIER    NOT NULL,
                                    LastUpdated         DATETIME            NOT NULL,
                                    SessionExpiryDate   DATETIME            NULL
                        ) ON [PRIMARY];
GO

CREATE UNIQUE NONCLUSTERED INDEX [IX_SessionId] ON dbo.PersonSession (SessionId)  ON [PRIMARY];
GO

ALTER TABLE dbo.PersonSession 
ADD CONSTRAINT [FK_PersonSession_Person] FOREIGN KEY (PersonId) REFERENCES dbo.Person (PersonId);
GO

GO
SET NOCOUNT ON;
DECLARE @i INT = 0;

WHILE @i < 1000
BEGIN
    SET @i += 1;
    INSERT INTO dbo.Person (PersonId, LoginId)
    VALUES  (@i, 'xxxxxxxxxxxx' + CAST(@i AS VARCHAR(5)));

    INSERT INTO dbo.PersonSession   (   PersonId, SessionId, LastUpdated, SessionExpiryDate)
    VALUES      (   @i, NEWID(), GETUTCDATE(), DATEADD(MINUTE, 30, GETUTCDATE()));


END
GO

--Get the SessionId guid to paste into the other queries
SELECT SessionId 
FROM dbo.PersonSession 
WHERE PersonId = 100
Run Code Online (Sandbox Code Playgroud)

将此代码粘贴到新的查询窗口中以模拟读写过程步骤

--Read-write query window

--Pre-test steps
USE [LockingRepro];
GO
SET NOCOUNT ON
SET TRANSACTION ISOLATION LEVEL READ COMMITTED 

SELECT @@SPID -- Grab the Spid to use in the lock querying window
------------------------------

--1
BEGIN TRAN

--2
    SELECT * --
    FROM dbo.PersonSession WITH (ROWLOCK, UPDLOCK, HOLDLOCK)
    WHERE SessionId = 'Paste the SessionId for PersonId 100 here'

--5
    DELETE d
    FROM dbo.PersonSession  AS d WITH (ROWLOCK)
    WHERE PersonId = 100


-- Clean up
IF @@TRANCOUNT > 0 ROLLBACK

Run Code Online (Sandbox Code Playgroud)

将此代码粘贴到新的查询窗口中以模拟只读过程步骤

-- Read only query window

--Pre-test steps
USE [LockingRepro];
GO

SET TRANSACTION ISOLATION LEVEL READ COMMITTED
SET NOCOUNT ON
SELECT @@SPID
-----------------------------------------


--3
BEGIN TRAN

--4
SELECT PersonId
FROM    dbo.PersonSession WITH (ROWLOCK, HOLDLOCK)
WHERE   SessionId = 'Paste the SessionId for PersonId 100 here'


--6
SELECT  PersonId, LastUpdated, SessionId, SessionExpiryDate
FROM dbo.PersonSession WITH (ROWLOCK) 
WHERE PersonId = 100


-- Clean up
IF @@TRANCOUNT > 0 ROLLBACK
Run Code Online (Sandbox Code Playgroud)

将此代码粘贴到单独的查询窗口并编辑以使用 ReadOnly 和 ReadWrtie 查询窗口中的 SPID

您需要在步骤 6 中的查询之后非常快速地执行此查询,以便能够在由于死锁而发生回滚之前捕获锁。

USE [LockingRepro];
GO

DECLARE @ReadOnlySpid   INT = nn, -- edit to specify the spids from the other query windows
        @ReadWriteSpid  INT = nn
SELECT  CASE    WHEN    request_session_id = @ReadOnlySpid THEN 
                    'ReadOnly'
                ELSE
                    'ReadWrite'
        END    AS [Connection],
        DB_NAME(resource_database_id) AS [Database],
        resource_type,
        CASE WHEN resource_database_id = DB_ID() THEN
                CASE    WHEN resource_type = 'OBJECT' THEN
                            OBJECT_SCHEMA_NAME(resource_associated_entity_id) + '.' + OBJECT_NAME(resource_associated_entity_id) 
                        WHEN resource_type IN( 'PAGE', 'KEY', 'EXTENT', 'RID', 'HOBT') THEN
                            (   SELECT OBJECT_SCHEMA_NAME(p.[object_id]) + '.' + OBJECT_NAME(p.[object_id])    
                                      + '.' + i.[name]
                                        + '  (Ptn:' + FORMAT(p.partition_number, '0') + ')'
                                FROM sys.partitions AS p
                                LEFT JOIN   sys.indexes AS i 
                                  ON    p.[object_id] = i.[object_id]
                                  AND   p.index_id = i.index_id
                                WHERE   [partition_id] = resource_associated_entity_id
                            )
                        WHEN resource_type = 'ALLOCATION_UNIT' THEN 
                            (   SELECT OBJECT_SCHEMA_NAME(p.[object_id]) + '.' + OBJECT_NAME(p.[object_id])    
                                        + '.' + i.[name]
                                        + '  (Ptn:' + FORMAT(p.partition_number, '0') + ' Alloc:' + au.type_desc COLLATE SQL_Latin1_General_CP1_CI_AS + ')'
                                FROM sys.partitions AS p
                                INNER JOIN  sys.allocation_units AS au
                                  ON    p.[partition_id] = au.container_id
                                LEFT JOIN   sys.indexes AS i 
                                  ON    p.[object_id] = i.[object_id]
                                  AND   p.index_id = i.index_id
                                WHERE   au.allocation_unit_id = resource_associated_entity_id
                            )
                    END
            ELSE
                CAST(resource_associated_entity_id AS VARCHAR(100))
        END AS [Entity],
        resource_associated_entity_id,
        resource_description,
        resource_lock_partition,
        request_mode,
        request_type,
        request_status,
        request_reference_count        
FROM    sys.dm_tran_locks
WHERE   request_session_id IN (@ReadOnlySpid, @ReadWriteSpid)
AND     resource_type <> 'DATABASE'
ORDER BY request_session_id

Run Code Online (Sandbox Code Playgroud)

预期行为的锁定查询输出示例,即发生死锁

步骤 1 - ReadWrite 连接启动一个事务,因此此时没有锁定

步骤 2 - ReadWrite 连接使用 UPDLOCK 执行 SELECT 并获取非聚集索引和聚集索引上的 KEY 级 U 锁

第 3 步 - ReadOnly 连接启动一个事务 - 没有更改锁

第 4 步 - Readonly 连接使用 HOLDLOCK 执行 SELECT - 它只需要访问非聚集索引并获取该索引中 KEY 上的 S 锁

第 5 步 - ReadWrite 连接使用聚集键执行 DELETE - 它获取聚集索引键上的 X 锁,但在等待将非聚集索引键上的 U 锁转换为 X 锁时被阻塞,因为 ReadOnly 连接持有一个S 锁定它

步骤 6 - ReadOnly 连接使用聚集索引键执行 SELECT 并被阻塞,等待获取聚集索引键上的 S 锁,ReadWrite 持有 X 锁,并在几秒钟内检测到死锁

意外行为的锁查询输出示例 在 这种情况下,步骤 6 中的 ReadOnly 连接 SELECT 返回其结果并且没有发生死锁或阻塞,即使聚集索引键已授予 ReadWrite 连接 X 锁。

步骤 1 到 5 的所有输出都与上述预期行为示例相同,因此我不会重复。

这是第 6 步的输出 - 它没有在 ReadOnly 连接的聚集索引键上显示 S 锁

该查询快速返回结果,因此我无法捕获它是否确实获得了共享锁,但可以重复执行查询,而聚集索引上的 X 锁显然仍由 ReadWrite 连接持有。

Jos*_*ell 3

我设置了以下扩展事件会话来捕获在步骤 6 中获取的锁,并筛选到该 spid(对我来说是 55):

\n\n
CREATE EVENT SESSION [locks] ON SERVER \nADD EVENT sqlserver.lock_acquired(\n    ACTION(sqlserver.session_id,sqlserver.sql_text)\n    WHERE ([sqlserver].[session_id]=(55)))\nADD TARGET package0.ring_buffer\nWITH (STARTUP_STATE=OFF)\nGO\n
Run Code Online (Sandbox Code Playgroud)\n\n

所采取的锁定是页面上的意图共享 (IS) 锁定(尽管ROWLOCK提示)。

\n\n

lock_acquired 事件的屏幕截图

\n\n

这与您提供的查询结果中的 X 锁定 object_id 相匹配sys.dm_tran_locks

\n\n

所有锁定信息的屏幕截图

\n\n

有关详细信息,请参阅 Paul White 的文章“丢失共享锁的情况”,但这是一种特定的优化,可以绕过您期望的阻塞:

\n\n
\n

SQL Server 包含一项优化,可以避免在正确的情况下获取行级共享 (S) 锁。具体来说,如果没有共享锁,不存在读取未提交数据的风险,那么它可以跳过共享锁。

\n
\n\n

步骤 6 中的查询在页面上获取 IS 锁,跳过对键(行)获取 S 锁的正常第一步。

\n\n

演示代码中此行为的间歇性可能是由于并不总是使用ROWLOCK 提示:

\n\n
\n

ROWLOCK 确实是一个提示,而不是一个指令 \xe2\x80\x94 存储引擎可能会或可能不会尊重它。

\n
\n\n

或者数据页上还有其他未提交的更改(这不是您的重现中的情况,但可能是您真实场景中的情况):

\n\n
\n

如果同一页上存在未提交的更改,则 SQL Server 无法应用锁定优化。

\n
\n\n

顺便说一句,我在我的笔记本电脑(SQL Server 2017 CU13)上根本没有经历过死锁行为。

\n