T-SQL是对更新的更新限制原子的子查询吗?
-
28-09-2019 - |
题
我在MS SQL Server 2008 R2中有一个简单的队列实现。这是队列的精髓:
CREATE TABLE ToBeProcessed
(
Id BIGINT IDENTITY(1,1) PRIMARY KEY NOT NULL,
[Priority] INT DEFAULT(100) NOT NULL,
IsBeingProcessed BIT default (0) NOT NULL,
SomeData nvarchar(MAX) NOT null
)
我想原子选择按优先级和ID订购的顶部n行是错误的,并更新这些行以说它们正在处理。我以为我会使用更新,顶部,输出和订单的组合,但不幸的是,您无法在更新语句中使用顶部和订单。
因此,我已经制作了一个条款来限制更新,该子查询通过订单(见下文)。我的问题是,整个陈述是原子,还是我需要将其包裹在交易中?
DECLARE @numberToProcess INT = 2
CREATE TABLE #IdsToProcess
(
Id BIGINT NOT null
)
UPDATE
ToBeProcessed
SET
ToBeProcessed.IsBeingProcessed = 1
OUTPUT
INSERTED.Id
INTO
#IdsToProcess
WHERE
ToBeProcessed.Id IN
(
SELECT TOP(@numberToProcess)
ToBeProcessed.Id
FROM
ToBeProcessed
WHERE
ToBeProcessed.IsBeingProcessed = 0
ORDER BY
ToBeProcessed.Id,
ToBeProcessed.Priority DESC)
SELECT
*
FROM
#IdsToProcess
DROP TABLE #IdsToProcess
这是一些插入一些虚拟行的SQL:
INSERT INTO ToBeProcessed (SomeData) VALUES (N'');
INSERT INTO ToBeProcessed (SomeData) VALUES (N'');
INSERT INTO ToBeProcessed (SomeData) VALUES (N'');
INSERT INTO ToBeProcessed (SomeData) VALUES (N'');
INSERT INTO ToBeProcessed (SomeData) VALUES (N'');
解决方案
如果我理解问题的动机,您想避免两项并发交易都可以执行子查询以使顶n行进行处理,然后继续更新相同的行?
在这种情况下,我会使用这种方法。
;WITH cte As
(
SELECT TOP(@numberToProcess)
*
FROM
ToBeProcessed WITH(UPDLOCK,ROWLOCK,READPAST)
WHERE
ToBeProcessed.IsBeingProcessed = 0
ORDER BY
ToBeProcessed.Id,
ToBeProcessed.Priority DESC
)
UPDATE
cte
SET
IsBeingProcessed = 1
OUTPUT
INSERTED.Id
INTO
#IdsToProcess
我早些时候有点不确定SQL Server是否会服用 U
使用子查询处理您的版本时锁定锁定,从而阻止了两项并发交易,从而读取相同的读取 TOP N
行。事实并非如此。
测试表
CREATE TABLE JobsToProcess
(
priority INT IDENTITY(1,1),
isprocessed BIT ,
number INT
)
INSERT INTO JobsToProcess
SELECT TOP (1000000) 0,0
FROM master..spt_values v1, master..spt_values v2
测试脚本(在2个并发SSMS会话中运行)
BEGIN TRY
DECLARE @FinishedMessage VARBINARY (128) = CAST('TestFinished' AS VARBINARY (128))
DECLARE @SynchMessage VARBINARY (128) = CAST('TestSynchronising' AS VARBINARY (128))
SET CONTEXT_INFO @SynchMessage
DECLARE @OtherSpid int
WHILE(@OtherSpid IS NULL)
SELECT @OtherSpid=spid
FROM sys.sysprocesses
WHERE context_info=@SynchMessage and spid<>@@SPID
SELECT @OtherSpid
DECLARE @increment INT = @@spid
DECLARE @number INT = @increment
WHILE (@number = @increment AND NOT EXISTS(SELECT * FROM sys.sysprocesses WHERE context_info=@FinishedMessage))
UPDATE JobsToProcess
SET @number=number +=@increment,isprocessed=1
WHERE priority = (SELECT TOP 1 priority
FROM JobsToProcess
WHERE isprocessed=0
ORDER BY priority DESC)
SELECT *
FROM JobsToProcess
WHERE number not in (0,@OtherSpid,@@spid)
SET CONTEXT_INFO @FinishedMessage
END TRY
BEGIN CATCH
SET CONTEXT_INFO @FinishedMessage
SELECT ERROR_MESSAGE(), ERROR_NUMBER()
END CATCH
几乎立即执行停止,因为两个并发交易都更新了同一行,因此 S
识别的锁 TOP 1 priority
必须在aquires a之前被释放 U
锁定,然后进行2次交易来获取行 U
和 X
锁定序列。
如果添加了CI ALTER TABLE JobsToProcess ADD PRIMARY KEY CLUSTERED (priority)
然后,僵局几乎立即发生,就像在这种情况下一样 S
锁没有发布,一项交易aquires a U
锁定行,等待将其转换为 X
锁定,另一笔交易仍在等待转换 S
锁定到 U
锁。
如果上面的查询已更改为使用 MIN
而不是 TOP
WHERE priority = (SELECT MIN(priority)
FROM JobsToProcess
WHERE isprocessed=0
)
然后SQL Server设法从计划中完全消除了子查询并采取 U
一直锁定。
其他提示
根据我的所有经验和我读过的所有文档,每个T-SQL的陈述都应该是原子。您拥有的单个T-SQL语句,Ergo应该是原子,并且不需要明确的事务语句。我多次使用过这种精确的逻辑,从来没有遇到过问题。我期待看到是否有人作为支持的替代意见。
顺便说一句,查看排名函数,特别是row_number(),以检索设定的项目数量。语法可能有点尴尬,但总体而言,它们是灵活而强大的工具。 (大约有一个少数数十亿个堆叠的问题和答案来讨论它们。)