如果一个表充当队列,我该如何最好地配置表/查询,以便多个客户端同时从队列中处理?
例如,下表显示了工作人员必须处理的命令。当worker完成后,它会将处理后的值设置为true。
| ID | COMMAND | PROCESSED |
| 1 | ... | true |
| 2 | ... | false |
| 3 | ... | false |
客户端可能会获得一个命令来处理:
select top 1 COMMAND
from EXAMPLE_TABLE
with (UPDLOCK, ROWLOCK)
where PROCESSED=false;
但是,如果有多个worker,则每个都尝试获取ID = 2的行。只有第一个会得到悲观的锁定,其余的将等待。然后其中一个将获得第3行等等。
哪些查询/配置允许每个工作者客户端分别获取不同的行并同时处理它们?
编辑:
有几个答案表明使用表格本身记录进行中状态的变化。我认为在单一交易中这是不可能的。 (如果在txn提交之前没有其他工作人员会看到它,那么更新状态的重点是什么?)或许建议是:
# start transaction
update to 'processing'
# end transaction
# start transaction
process the command
update to 'processed'
# end transaction
这是人们通常接近这个问题的方式吗?在我看来,如果可能的话,DB会更好地处理问题。
答案 0 :(得分:51)
我建议你过去Using tables as Queues。
正确实施的队列可以处理数千个并发用户,并且每分钟可以服务高达1/2百万的入队/出队操作。在SQL Server 2005之前,解决方案很麻烦,并且在单个事务中混合使用SELECT
和UPDATE
并提供恰当的锁定提示组合,如gbn链接的文章中所述。幸运的是,自SQL Server 2005出现OUTPUT子句以来,可以使用更优雅的解决方案,现在MSDN建议使用OUTPUT clause:
您可以在应用程序中使用OUTPUT 使用表作为队列,或持有 中间结果集。那就是 应用程序不断添加或 从表中删除行
基本上,为了使其以高度并发的方式工作,您需要完成3个难题:
1)你需要以原子方式出局。你必须找到行,skipp任何锁定的行,并在单个原子操作中将其标记为“dequeued”,这就是OUTPUT
子句发挥作用的地方:
with CTE as (
SELECT TOP(1) COMMAND, PROCESSED
FROM TABLE WITH (READPAST)
WHERE PROCESSED = 0)
UPDATE CTE
SET PROCESSED = 1
OUTPUT INSERTED.*;
2)您必须使用PROCESSED
列上最左侧的聚集索引键构建您的表。如果ID
使用了主键,则将其作为群集键中的第二列移动。关于是否在ID
列上保留非群集密钥的争论是开放的,但我强烈支持不在队列上有任何辅助非聚集索引:
CREATE CLUSTERED INDEX cdxTable on TABLE(PROCESSED, ID);
3)您不能通过任何其他方式查询此表,而是通过Dequeue查询。尝试进行Peek操作或尝试将该表作为存储和用作存储将非常可能导致死锁,并将显着降低吞吐量。
基于处理位的原子出队,READPAST提示搜索元素到队列以及最左边的密钥在聚合索引上的组合确保了在高度并发负载下的非常高的吞吐量。
答案 1 :(得分:8)
我在这里的回答向您展示了如何将表格用作队列...... SQL Server Process Queue Race Condition
你基本上需要“ROWLOCK,READPAST,UPDLOCK”提示
答案 2 :(得分:1)
如果要为多个客户端序列化操作,只需使用应用程序锁即可。
BEGIN TRANSACTION
EXEC sp_getapplock @resource = 'app_token', @lockMode = 'Exclusive'
-- perform operation
EXEC sp_releaseapplock @resource = 'app_token'
COMMIT TRANSACTION
答案 3 :(得分:0)
您可以使用int来定义命令的状态,而不是使用Processed的布尔值:
1 = not processed
2 = in progress
3 = complete
然后每个工作人员将获得Processed = 1的下一行,将Processed更新为2然后开始工作。当完成Processed中的工作更新为3.此方法还允许扩展其他已处理结果,例如,您可以为“已成功完成”和“已完成错误”添加新状态,而不仅仅定义工作者是否为完成状态< / p>
答案 4 :(得分:0)
可能更好的选择是使用trisSate处理列以及版本/时间戳列。然后,处理列中的三个值将指示是否正在处理,处理或未处理行。
例如
CREATE TABLE Queue ID INT NOT NULL PRIMARY KEY,
Command NVARCHAR(100),
Processed INT NOT NULL CHECK (Processed in (0,1,2) ),
Version timestamp)
您获取前1个未处理的行,将状态设置为欠处理,并在完成任务后将状态设置回处理状态。将您的更新状态基于版本和主键列。如果更新失败,则有人已经在那里。
您可能还想添加一个客户端标识符,这样如果客户端在处理它时死亡,它可以重新启动,查看最后一行,然后从它的位置开始。
答案 5 :(得分:0)
我会远离桌子上的锁。只需创建两个额外的列,如IsProcessing(bit / boolean)和ProcessingStarted(datetime)。当工作人员在超时后崩溃或没有更新他的行时,您可以让另一名工作人员尝试处理数据。
答案 6 :(得分:0)
一种方法是使用单个更新语句标记行。如果您在where
子句中读取状态并在set
子句中更改它,则不会介入其他进程,因为该行将被锁定。例如:
declare @pickup_id int
set @pickup_id = 1
set rowcount 1
update YourTable
set status = 'picked up'
, @pickup_id = id
where status = 'new'
set rowcount 0
return @pickup_id
这使用rowcount
最多更新一行。如果未找到任何行,@pickup_id
将为-1
。