这些SQL Server过程并发安全吗?

时间:2019-11-21 10:54:49

标签: sql-server tsql

我有下表,其中包含工作单元:

create table [dbo].[Queue]
(
    [QueueId] bigint not null identity(1,1),
    [UserId] bigint default null, -- irrelevant foreign key here
    primary key clustered ([QueueId] asc)
);
go

一包工人不断在这张桌子上盘旋,通过将UserId字段从null更改为正值来获取一个或多个工作单元。没有两个工作人员不能同时更新相同的QueueId,并且他们不应该等待(readpast应该可以帮助您)

以下方法使添加工作到表变得容易:

/**
* Push some work units.
* (rewritten from basic while insert to @Larnu's Tally suggestion)
*/
create procedure [dbo].[spPushWork]
    @Count int
as
begin
    if @Count < 1 or @Count > 1000000 throw 50001, N'@Count must be 1-1M.', 1;

    with [num] as
    (
        select [num] from (values (null),(null),(null),(null),(null),(null),(null),(null),(null),(null)) [num]([num])
    ), [tally] as
    (
        select top (@Count)
            row_number() over (order by (select null)) as [ind]
        from [num] [num1]
            cross join [num] [num2]
            cross join [num] [num3]
            cross join [num] [num4]
            cross join [num] [num5]
            cross join [num] [num6]
    )
    merge into [dbo].[queue]
    using (select [ind] from [tally]) [t]
    on 1 = 0
    when not matched then insert default values;
end
go

现在我们有2种方法来进行工作。

方法1 是线程安全的(我希望),因为它是一个选择更新组合:

/**
* This grabs work units in a single operation (Select + Update).
*/
create procedure [dbo].[spGrabSafe]
    @UserId bigint
    ,@Count int = 1
as
begin
    if @UserId < 1 throw 50001, N'@UserId must be 1+.', 1;
    if @Count < 1 throw 50001, N'@Count must be 1+.', 2;

    declare @Ids table ([Id] bigint not null);

    -- fetch and claim via single query
    with [cte] as
    (
        select top(@Count) [QueueId]
        from [dbo].[Queue] with (readpast) -- skip locked
        where [UserId] is null
        order by [QueueId] asc
    )
    update [q]
    set [UserId] = @UserId
    output [inserted].[QueueId] into @Ids
    from [dbo].[Queue] [q]
    join [cte] on [cte].[QueueId] = [q].[QueueId];

    select [Id] from @Ids;
end;
go

方法#2 通过首先锁定行,然后通过更改UserId来声明它们与方法二分之一操作的作用相同。它还有一个delay参数,使我们可以使其运行时间更长以进行测试:

/**
* This grabs work units in multiple operations (Select&lock + Update).
*/
create procedure [dbo].[spGrabUnsafe]
    @UserId bigint
    ,@Count int = 1
    ,@Delay time = null
as
begin
    if @UserId < 1 throw 50001, N'@UserId must be 1+.', 1;
    if @Count < 1 throw 50001, N'@Count must be 1+.', 1;

    declare @Ids table ([Id] bigint not null);

    begin transaction
        -- fetch the QueueId's
        insert into @Ids
        select top(@Count) [QueueId] 
        from [dbo].[Queue]
        with (xlock, rowlock, readpast) -- lock rows + skip locked
        where [UserId] is null
        order by [QueueId] asc;

        -- claim via UserId
        update [q]
        set [UserId] = @UserId
        from [dbo].[Queue] [q]
        join @Ids [ids] on [ids].[Id] = [q].[QueueId];

        -- this allows to wait a bit to test concurrency
        if @Delay is not null
        begin
            declare @Time varchar(12) = convert(varchar, @Delay, 114);
            waitfor delay @Time;
        end;
    commit transaction;

    select [Id] from @Ids;
end
go

方法2在并发环境中是否安全?选择和更新UserId之间存在差距。但是这些行应该被锁定...

0 个答案:

没有答案