为什么原子语句需要锁提示?
Why are lock hints needed on an atomic statement?
问题
对以下语句应用锁有什么好处?
同样,如果我们不包括这些提示,我们会看到什么问题?也就是说,它们是防止竞争条件、提高性能还是其他什么?询问它们可能是为了防止一些我没有考虑过的问题,而不是我假设的竞争条件。
注意:这是此处提出的问题的溢出:SQL Threadsafe UPDATE TOP 1 for FIFO Queue
有问题的声明
WITH nextRecordToProcess AS
(
SELECT TOP(1) Id, StatusId
FROM DemoQueue
WHERE StatusId = 1 --Ready for processing
ORDER BY DateSubmitted, Id
)
UPDATE nextRecordToProcess
SET StatusId = 2 --Processing
OUTPUT Inserted.Id
要求
- SQL 用于从队列中检索未处理的记录。
- 要获取的记录应该是队列中状态为Ready(StatusId = 1)的第一条记录。
- 可能有多个 workers/sessions 正在处理来自该队列的消息。
- 我们要确保队列中的每条记录只被拾取一次(即由一个工作人员),并且每个工作人员按照消息在队列中出现的顺序处理消息。
- 一个工人比另一个工人工作得更快是可以的(即如果工人 A 拿起记录 1 然后工人 B 拿起记录 2 如果工人 B 在工人 A 完成处理记录 1 之前完成记录 2 的处理是可以的).我们只关心获取记录的上下文。
- 没有正在进行的交易;即我们只想从队列中取出记录;在我们回来将状态从
Processing
提升到 Processed
之前,我们不需要保持锁定状态。
附加 SQL 上下文:
CREATE TABLE Statuses
(
Id SMALLINT NOT NULL PRIMARY KEY CLUSTERED
, Name NVARCHAR(32) NOT NULL UNIQUE
)
GO
INSERT Statuses (Id, Name)
VALUES (0,'Draft')
, (1,'Ready')
, (2,'Processing')
, (3,'Processed')
, (4,'Error')
GO
CREATE TABLE DemoQueue
(
Id BIGINT NOT NULL IDENTITY(1,1) PRIMARY KEY CLUSTERED
, StatusId SMALLINT NOT NULL FOREIGN KEY REFERENCES Statuses(Id)
, DateSubmitted DATETIME --will be null for all records with status 'Draft'
)
GO
建议声明
在各种讨论队列的博客中,以及引起此讨论的问题中,建议将上述语句更改为包含如下锁提示:
WITH nextRecordToProcess AS
(
SELECT TOP(1) Id, StatusId
FROM DemoQueue WITH (UPDLOCK, ROWLOCK, READPAST)
WHERE StatusId = 1 --Ready for processing
ORDER BY DateSubmitted, Id
)
UPDATE nextRecordToProcess
SET StatusId = 2 --Processing
OUTPUT Inserted.Id
我的理解
我知道需要锁定这些提示的好处是:
- UPDLOCK:因为我们正在 selecting 记录以更新它的状态,所以我们需要确保在我们读取它之后但在我们更新它之前读取该记录的任何其他会话不会能够读取记录以更新它(或者更确切地说,这样的语句必须等到我们执行了更新并释放了锁,然后另一个会话才能看到我们的记录及其新值)。
- ROWLOCK:当我们锁定记录时,我们希望确保我们的锁定只影响我们锁定的行;即因为我们不需要锁定很多资源/我们不想影响其他进程/我们希望其他会话能够读取队列中的下一个可用项目,即使该项目与我们锁定的记录在同一页面中.
- READPAST:如果另一个会话已经在从队列中读取一个项目,而不是等待该会话释放它的锁,我们的会话应该选择队列中的下一个可用(未锁定)记录。
即如果我们 运行 下面的代码我认为这是有道理的:
DECLARE @nextRecordToProcess BIGINT
BEGIN TRANSACTION
SELECT TOP (1) @nextRecordToProcess = Id
FROM DemoQueue WITH (UPDLOCK, ROWLOCK, READPAST)
WHERE StatusId = 1 --Ready for processing
ORDER BY DateSubmitted, Id
--and then in a separate statement
UPDATE DemoQueue
SET StatusId = 2 --Processing
WHERE Id = @nextRecordToProcess
COMMIT TRANSACTION
--@nextRecordToProcess is then returned either as an out parameter or by including a `select @nextRecordToProcess Id`
然而,当 select 和更新出现在同一条语句中时,我假设没有其他会话可以在我们会话的读取和更新之间读取相同的记录;所以不需要明确的锁定提示。
我是否从根本上误解了锁的工作原理?或者这些提示的建议是否与其他一些相似但不同的用例相关?
tl;博士
用于高并发专用队列table场景下的性能优化。
详细
我想我已经通过寻找 related SO answer by this quoted blog's 作者找到了答案。
这个建议似乎是针对一个非常具体的场景;其中 table 用作队列 dedicated 作为队列;即 table 不用于任何其他目的。在这种情况下,锁定提示是有意义的。它们与防止竞争条件无关;他们将通过避免(非常短期的)阻塞来提高高并发场景中的性能。
ReadPast
锁提高了高并发场景下的性能;无需等待当前读取的记录被释放;唯一锁定它的将是另一个 "Queue Worker" 进程,因此我们可以安全地跳过知道该工作人员正在处理此记录。
RowLock
确保我们一次不会锁定超过一行,因此下一个请求消息的工作人员将获得下一条记录,而不是跳过几条记录,因为它们在一个锁定记录的页面。
UpdLock
用于获取锁;即 RowLock
说要锁定什么但没有说必须有锁,并且 ReadPast
确定遇到其他锁定记录时的行为,因此不会再次导致锁定当前记录。我怀疑这不是明确需要的,因为 SQL 无论如何都会在后台获取它(事实上,在链接的 SO 答案中只指定了 ReadPast
);但被包含在 post 块中是为了完整性/显式显示 SQL 无论如何都会在后台隐式导致的锁定。
但是 post 是为 专用队列 table 编写的。在 table 用于其他用途的地方(例如,在 the original question 中,它是一个 table 持有发票数据,恰好有一列用于跟踪已打印的内容),该建议可能不可取。即通过使用 ReadPast
锁,您将跳过所有锁定的记录;并且不能保证这些记录被处理您队列的另一个工作人员锁定;它们可能出于某些完全不相关的目的而被锁定。这将打破 FIFO 要求。
鉴于此,我认为 my answer 支持链接问题。即要么创建一个专用的 table 来处理队列场景,要么考虑其他选项及其在上下文或您的场景中的优缺点。
约翰是对的,因为这些都是优化,但在 SQL 世界中,这些优化可能意味着 'fast' 与 'unbearable size-of-data slow' 之间的差异 and/or 'works' 与 'unusable deadlock mess'.
过去的提示很清楚。对于另外两个,我觉得我需要添加更多的上下文:
- ROWLOCK提示是为了防止页锁粒度扫描。锁定粒度(行与页)是在查询开始时预先确定的,并且基于对查询将扫描的页数的估计(第三个粒度,table,仅在特殊情况下使用,并且不适用于此处)。通常,出列操作永远不必扫描如此多的页面,以便引擎考虑页面粒度。但是我已经看到 'in the wild' 引擎决定使用页面锁定粒度的情况,这会导致出队中的阻塞和死锁
需要UPDLOCK来防止升级锁死锁的情况。 UPDATE 语句在逻辑上分为搜索需要更新的行然后更新行。搜索需要锁定它评估的行。如果该行符合条件(满足 WHERE 条件),则更新该行,并且更新始终是独占锁。所以问题是你如何在搜索过程中锁定行?如果您使用共享锁,则两个 UPDATE 将查看同一行(它们可以,因为共享锁允许它们),两者都确定该行符合条件,并且都尝试将锁升级为独占 -> 死锁。如果在搜索过程中使用排他锁,则不会发生死锁,但是 UPDATE 将在与任何其他读取评估的所有行上发生冲突,即使该行不符合条件(更不用说排他锁不能提前释放 w/o打破 two-phase-locking)。这就是为什么有一个 U 模式锁,它与 Shared 兼容(这样候选行的 UPDATE 评估不会阻塞读取)但与另一个 U 不兼容(这样两个 UPDATE 就不会死锁)。典型的基于 CTE 的出队需要这个提示有两个原因:
- 因为是 CTE,查询处理并不总是理解 CTE 内的 SELECT 是 UPDATE 的目标,应该使用 U 模式锁和
- 出列操作将始终在更新相同的行(行是 'dequeued')之后进行,因此经常发生死锁。
问题
对以下语句应用锁有什么好处?
同样,如果我们不包括这些提示,我们会看到什么问题?也就是说,它们是防止竞争条件、提高性能还是其他什么?询问它们可能是为了防止一些我没有考虑过的问题,而不是我假设的竞争条件。
注意:这是此处提出的问题的溢出:SQL Threadsafe UPDATE TOP 1 for FIFO Queue
有问题的声明
WITH nextRecordToProcess AS
(
SELECT TOP(1) Id, StatusId
FROM DemoQueue
WHERE StatusId = 1 --Ready for processing
ORDER BY DateSubmitted, Id
)
UPDATE nextRecordToProcess
SET StatusId = 2 --Processing
OUTPUT Inserted.Id
要求
- SQL 用于从队列中检索未处理的记录。
- 要获取的记录应该是队列中状态为Ready(StatusId = 1)的第一条记录。
- 可能有多个 workers/sessions 正在处理来自该队列的消息。
- 我们要确保队列中的每条记录只被拾取一次(即由一个工作人员),并且每个工作人员按照消息在队列中出现的顺序处理消息。
- 一个工人比另一个工人工作得更快是可以的(即如果工人 A 拿起记录 1 然后工人 B 拿起记录 2 如果工人 B 在工人 A 完成处理记录 1 之前完成记录 2 的处理是可以的).我们只关心获取记录的上下文。
- 没有正在进行的交易;即我们只想从队列中取出记录;在我们回来将状态从
Processing
提升到Processed
之前,我们不需要保持锁定状态。
附加 SQL 上下文:
CREATE TABLE Statuses
(
Id SMALLINT NOT NULL PRIMARY KEY CLUSTERED
, Name NVARCHAR(32) NOT NULL UNIQUE
)
GO
INSERT Statuses (Id, Name)
VALUES (0,'Draft')
, (1,'Ready')
, (2,'Processing')
, (3,'Processed')
, (4,'Error')
GO
CREATE TABLE DemoQueue
(
Id BIGINT NOT NULL IDENTITY(1,1) PRIMARY KEY CLUSTERED
, StatusId SMALLINT NOT NULL FOREIGN KEY REFERENCES Statuses(Id)
, DateSubmitted DATETIME --will be null for all records with status 'Draft'
)
GO
建议声明
在各种讨论队列的博客中,以及引起此讨论的问题中,建议将上述语句更改为包含如下锁提示:
WITH nextRecordToProcess AS
(
SELECT TOP(1) Id, StatusId
FROM DemoQueue WITH (UPDLOCK, ROWLOCK, READPAST)
WHERE StatusId = 1 --Ready for processing
ORDER BY DateSubmitted, Id
)
UPDATE nextRecordToProcess
SET StatusId = 2 --Processing
OUTPUT Inserted.Id
我的理解
我知道需要锁定这些提示的好处是:
- UPDLOCK:因为我们正在 selecting 记录以更新它的状态,所以我们需要确保在我们读取它之后但在我们更新它之前读取该记录的任何其他会话不会能够读取记录以更新它(或者更确切地说,这样的语句必须等到我们执行了更新并释放了锁,然后另一个会话才能看到我们的记录及其新值)。
- ROWLOCK:当我们锁定记录时,我们希望确保我们的锁定只影响我们锁定的行;即因为我们不需要锁定很多资源/我们不想影响其他进程/我们希望其他会话能够读取队列中的下一个可用项目,即使该项目与我们锁定的记录在同一页面中.
- READPAST:如果另一个会话已经在从队列中读取一个项目,而不是等待该会话释放它的锁,我们的会话应该选择队列中的下一个可用(未锁定)记录。
即如果我们 运行 下面的代码我认为这是有道理的:
DECLARE @nextRecordToProcess BIGINT
BEGIN TRANSACTION
SELECT TOP (1) @nextRecordToProcess = Id
FROM DemoQueue WITH (UPDLOCK, ROWLOCK, READPAST)
WHERE StatusId = 1 --Ready for processing
ORDER BY DateSubmitted, Id
--and then in a separate statement
UPDATE DemoQueue
SET StatusId = 2 --Processing
WHERE Id = @nextRecordToProcess
COMMIT TRANSACTION
--@nextRecordToProcess is then returned either as an out parameter or by including a `select @nextRecordToProcess Id`
然而,当 select 和更新出现在同一条语句中时,我假设没有其他会话可以在我们会话的读取和更新之间读取相同的记录;所以不需要明确的锁定提示。
我是否从根本上误解了锁的工作原理?或者这些提示的建议是否与其他一些相似但不同的用例相关?
tl;博士
用于高并发专用队列table场景下的性能优化。
详细
我想我已经通过寻找 related SO answer by this quoted blog's 作者找到了答案。
这个建议似乎是针对一个非常具体的场景;其中 table 用作队列 dedicated 作为队列;即 table 不用于任何其他目的。在这种情况下,锁定提示是有意义的。它们与防止竞争条件无关;他们将通过避免(非常短期的)阻塞来提高高并发场景中的性能。
ReadPast
锁提高了高并发场景下的性能;无需等待当前读取的记录被释放;唯一锁定它的将是另一个 "Queue Worker" 进程,因此我们可以安全地跳过知道该工作人员正在处理此记录。RowLock
确保我们一次不会锁定超过一行,因此下一个请求消息的工作人员将获得下一条记录,而不是跳过几条记录,因为它们在一个锁定记录的页面。UpdLock
用于获取锁;即RowLock
说要锁定什么但没有说必须有锁,并且ReadPast
确定遇到其他锁定记录时的行为,因此不会再次导致锁定当前记录。我怀疑这不是明确需要的,因为 SQL 无论如何都会在后台获取它(事实上,在链接的 SO 答案中只指定了ReadPast
);但被包含在 post 块中是为了完整性/显式显示 SQL 无论如何都会在后台隐式导致的锁定。
但是 post 是为 专用队列 table 编写的。在 table 用于其他用途的地方(例如,在 the original question 中,它是一个 table 持有发票数据,恰好有一列用于跟踪已打印的内容),该建议可能不可取。即通过使用 ReadPast
锁,您将跳过所有锁定的记录;并且不能保证这些记录被处理您队列的另一个工作人员锁定;它们可能出于某些完全不相关的目的而被锁定。这将打破 FIFO 要求。
鉴于此,我认为 my answer 支持链接问题。即要么创建一个专用的 table 来处理队列场景,要么考虑其他选项及其在上下文或您的场景中的优缺点。
约翰是对的,因为这些都是优化,但在 SQL 世界中,这些优化可能意味着 'fast' 与 'unbearable size-of-data slow' 之间的差异 and/or 'works' 与 'unusable deadlock mess'.
过去的提示很清楚。对于另外两个,我觉得我需要添加更多的上下文:
- ROWLOCK提示是为了防止页锁粒度扫描。锁定粒度(行与页)是在查询开始时预先确定的,并且基于对查询将扫描的页数的估计(第三个粒度,table,仅在特殊情况下使用,并且不适用于此处)。通常,出列操作永远不必扫描如此多的页面,以便引擎考虑页面粒度。但是我已经看到 'in the wild' 引擎决定使用页面锁定粒度的情况,这会导致出队中的阻塞和死锁
需要UPDLOCK来防止升级锁死锁的情况。 UPDATE 语句在逻辑上分为搜索需要更新的行然后更新行。搜索需要锁定它评估的行。如果该行符合条件(满足 WHERE 条件),则更新该行,并且更新始终是独占锁。所以问题是你如何在搜索过程中锁定行?如果您使用共享锁,则两个 UPDATE 将查看同一行(它们可以,因为共享锁允许它们),两者都确定该行符合条件,并且都尝试将锁升级为独占 -> 死锁。如果在搜索过程中使用排他锁,则不会发生死锁,但是 UPDATE 将在与任何其他读取评估的所有行上发生冲突,即使该行不符合条件(更不用说排他锁不能提前释放 w/o打破 two-phase-locking)。这就是为什么有一个 U 模式锁,它与 Shared 兼容(这样候选行的 UPDATE 评估不会阻塞读取)但与另一个 U 不兼容(这样两个 UPDATE 就不会死锁)。典型的基于 CTE 的出队需要这个提示有两个原因:
- 因为是 CTE,查询处理并不总是理解 CTE 内的 SELECT 是 UPDATE 的目标,应该使用 U 模式锁和
- 出列操作将始终在更新相同的行(行是 'dequeued')之后进行,因此经常发生死锁。