7

我在 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
)

我想原子地选择按优先级排序的前 n 行以及 IsBeingProcessed 为 false 的 id 并更新这些行以说明它们正在被处理。我以为我会使用 Update、Top、Output 和 Order By 的组合,但不幸的是,您不能在 Update 语句中使用 top 和 order by。

因此,我创建了一个 in 子句来限制更新,并且该子查询按顺序执行(见下文)。我的问题是,这整个语句是原子的,还是我需要将它包装在事务中?

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'');
4

2 回答 2

8

如果我理解您想要避免两个并发事务都可以执行子查询以处理前 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 ServerU在使用子查询处理您的版本时是否会锁定,从而阻止两个并发事务读取相同的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必须在获取锁之前释放U锁,然后两个事务继续获取行U并按X顺序锁定。

堆

如果添加了 CI,ALTER TABLE JobsToProcess ADD PRIMARY KEY CLUSTERED (priority)则死锁几乎立即发生,因为在这种情况下,行S锁没有被释放,一个事务获取U行上的锁并等待将其转换为X锁,而另一个事务仍在等待转换其S锁到U锁。

聚集索引

如果上面的查询改为使用MIN而不是TOP

WHERE priority = (SELECT MIN(priority)
                   FROM JobsToProcess 
                   WHERE isprocessed=0 
                   )

然后 SQL Server 设法从计划中完全消除子查询并U一直锁定。

在此处输入图像描述

于 2011-04-08T18:43:29.617 回答
2

根据我的所有经验和我读过的所有文档,每个单独的 T-SQL 语句都应该是原子的。你所拥有的是一条 T-SQL 语句,因此应该是原子的,不需要显式的事务语句。我多次使用这种精确的逻辑,从来没有遇到过问题。我期待看到是否有人作为可支持的替代意见。

顺便说一下,查看排名函数,特别是 row_number(),用于检索一组项目。语法可能有点尴尬,但总的来说它们是灵活而强大的工具。(有关于讨论它们的无数 Stack Overlow 问题和答案。)

于 2010-11-04T13:42:48.393 回答