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 锁
步骤 6 - ReadOnly 连接使用聚集索引键执行 SELECT 并被阻塞,等待获取聚集索引键上的 S 锁,ReadWrite 持有 X 锁,并在几秒钟内检测到死锁
意外行为的锁查询输出示例 在 这种情况下,步骤 6 中的 ReadOnly 连接 SELECT 返回其结果并且没有发生死锁或阻塞,即使聚集索引键已授予 ReadWrite 连接 X 锁。
步骤 1 到 5 的所有输出都与上述预期行为示例相同,因此我不会重复。
这是第 6 步的输出 - 它没有在 ReadOnly 连接的聚集索引键上显示 S 锁
该查询快速返回结果,因此我无法捕获它是否确实获得了共享锁,但可以重复执行查询,而聚集索引上的 X 锁显然仍由 ReadWrite 连接持有。
我设置了以下扩展事件会话来捕获在步骤 6 中获取的锁,并筛选到该 spid(对我来说是 55):
\n\nCREATE 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\nRun Code Online (Sandbox Code Playgroud)\n\n所采取的锁定是页面上的意图共享 (IS) 锁定(尽管ROWLOCK提示)。
这与您提供的查询结果中的 X 锁定 object_id 相匹配sys.dm_tran_locks:
有关详细信息,请参阅 Paul White 的文章“丢失共享锁的情况”,但这是一种特定的优化,可以绕过您期望的阻塞:
\n\n\n\n\nSQL Server 包含一项优化,可以避免在正确的情况下获取行级共享 (S) 锁。具体来说,如果没有共享锁,不存在读取未提交数据的风险,那么它可以跳过共享锁。
\n
步骤 6 中的查询在页面上获取 IS 锁,跳过对键(行)获取 S 锁的正常第一步。
\n\n演示代码中此行为的间歇性可能是由于并不总是使用ROWLOCK 提示:
\n\n\n\n\nROWLOCK 确实是一个提示,而不是一个指令 \xe2\x80\x94 存储引擎可能会或可能不会尊重它。
\n
或者数据页上还有其他未提交的更改(这不是您的重现中的情况,但可能是您真实场景中的情况):
\n\n\n\n\n如果同一页上存在未提交的更改,则 SQL Server 无法应用锁定优化。
\n
顺便说一句,我在我的笔记本电脑(SQL Server 2017 CU13)上根本没有经历过死锁行为。
\n