我有下表,其中包含工作单元:
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
之间存在差距。但是这些行应该被锁定...