我正在构建基于表的穷人FIFO消息队列,并尝试实现receiveAll
操作,其中将检索当前队列中的所有消息。
要收到一条消息,我会这样做:
WITH receiveCte AS (
SELECT TOP 1 body
FROM MyQueue WITH (ROWLOCK, READPAST)
ORDER BY id
)
DELETE FROM receiveCte
OUTPUT deleted.body;
据我所知,ORDER BY
子句是保证删除顺序所必需的,即使id
是具有聚簇索引的身份主键。
现在,要执行receiveAll
操作,我需要删除所有行ORDER BY id
,显然这在没有TOP
子句的情况下不起作用。
因此,我考虑执行未锁定的SELECT
行并锁定整个事务,然后继续DELETE
,但似乎我找不到方法锁定受整个事务的select影响的行。
BEGIN TRAN
DECLARE @msgCount int;
SELECT @msgCount = COUNT(*)
FROM MyQueue WITH (UPDLOCK, ROWLOCK, READPAST);
...
COMMIT TRAN
如果我执行上面的COMMIT TRAN
除外,然后在另一个连接中执行以下语句,它仍会返回所有行,而我期望因0
而返回READPAST
在行上持有UPDLOCK
的持续交易。
SELECT COUNT(*)
FROM MyQueue WITH (READPAST)
显然,我一定是做错了......
编辑#1:
@ king.code在这种情况下已经给出了完美的答案,但是我发现了发生了什么。
事实证明COUNT(*)
似乎忽略了锁定提示,因此不足以进行测试。
此外,您似乎需要XLOCK
来确保READPAST
能够完成工作。
编辑#2:
警告:SELECT TOP 100 PERCENT ... ORDER BY
不起作用,因为在这种情况下SQL Server似乎忽略了ORDER BY子句。但是,似乎我们可以使用变量来欺骗优化器。 SELECT TOP (@hundred) PERCENT
,但我不确定它有多可靠。
答案 0 :(得分:3)
我尝试了这个,它对我有用:
**编辑**
根据此technet article更新,其中声明:
"如果必须使用TOP以有意义的时间顺序删除行,则必须在子选择语句中将TOP与ORDER BY一起使用。"
<强>设置强>
CREATE TABLE MyQueue
(
ID INT Identity Primary Key,
Body VARCHAR(MAX)
)
INSERT INTO MyQueue
VALUES ('Message 1'),
('Message 2'),
('Message 3'),
('Message 4'),
('Message 5'),
('Message 6'),
('Message 7'),
('Message 8'),
('Message 9'),
('Message 10'),
('Message 11')
在一个查询分析器窗口中,我执行了此操作:
查询会话1
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
GO
Begin Tran
;WITH receiveCte AS (
SELECT TOP 1 body
FROM MyQueue WITH (READPAST)
ORDER BY id
)
DELETE FROM receiveCte WITH (ROWLOCK)
OUTPUT deleted.body;
-- note no COMMIT
查询会话2
--Second window
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
GO
BEGIN TRAN
DELETE FROM MYQueue WITH (ROWLOCK)
WHERE ID IN
(
SELECT TOP 100 PERCENT ID
FROM MyQueue WITH (READPAST)
ORDER BY Id
)
OUTPUT deleted.Body
-- Note again no COMMIT
然后在第三个窗口中:
查询会话3
SELECT COUNT(*)
FROM MyQueue WITH (READPAST)
正确返回了 0
的结果答案 1 :(得分:2)
据我所知,ORDER BY子句是保证删除顺序所必需的,即使id是具有聚簇索引的身份主键也是如此。
你是对的。
现在,要执行receiveAll操作,我需要删除所有行ORDER BY id,显然这在没有TOP子句的情况下不起作用。
请记住,您可以在PERCENT
中使用TOP
:
DECLARE @Hundred FLOAT = 100;
SELECT TOP (@Hundred) PERCENT body
FROM MyQueue WITH (ROWLOCK, READPAST)
ORDER BY id;
<强>更新强>
我刚做了一些测试。看起来,如果我ORDER BY
聚集索引,我会使用和不使用TOP(100) PERCENT
获得相同的执行计划。
如果我ORDER BY
另一列,即使我放置TOP(100) PERCENT
,我也会在执行计划中看到排序操作。所以看起来它没有被忽略。
无论如何,由于@Hundred
变量和TOP
表达式为FLOAT
,您可以尝试将其设置为类似99.99999
的内容,看看会发生什么。
答案 2 :(得分:2)
从DB的角度来看,如果要删除所有行,则删除任何特定顺序的行没有什么意义。没有订购的简单DELETE就好了。 如果要从应用程序端逐行处理,则启动可序列化事务,阻止整个表并根据ID逐行处理\ delete,无需订购。
答案 3 :(得分:2)
使用Broker Queues尝试此操作:
USE MASTER
CREATE DATABASE SBTest
GO
ALTER DATABASE SBTest SET ENABLE_BROKER;
GO
USE SBTest
GO
CREATE Message TYPE MyMessage
VALIDATION = NONE
GO
CREATE CONTRACT MyContract (MyMessage SENT BY INITIATOR)
GO
CREATE QUEUE MYSendQueue
GO
CREATE QUEUE MyReceiveQueue
GO
CREATE SERVICE MySendService
ON QUEUE MySendQueue (MyContract)
GO
CREATE SERVICE MyReceiveService
ON QUEUE MyReceiveQueue (MyContract)
GO
-- Send Messages
DECLARE @MyDialog uniqueidentifier
DECLARE @MyMessage NVARCHAR(128)
BEGIN DIALOG CONVERSATION @MyDialog
FROM SERVICE MySendService
TO SERVICE 'MyReceiveService'
ON CONTRACT MyContract
WITH ENCRYPTION = OFF
-- Send messages on Dialog
SET @MyMessage = N'My First Message';
SEND ON CONVERSATION @MyDialog
MESSAGE TYPE MyMessage (@MyMessage)
SET @MyMessage = N'My Second Message';
SEND ON CONVERSATION @MyDialog
MESSAGE TYPE MyMessage (@MyMessage)
SET @MyMessage = N'My Third Message';
SEND ON CONVERSATION @MyDialog
MESSAGE TYPE MyMessage (@MyMessage)
GO
-- View messages from Receive Queue
SELECT CONVERT(NVARCHAR(MAX), message_body) AS Message
FROM MyReceiveQueue
GO
-- Receive 1 message from Queue
RECEIVE TOP(1) CONVERT(NVARCHAR(MAX), message_body) AS Message
FROM MyReceiveQueue
GO
-- Receive All messages from Receive Queue
RECEIVE CONVERT(NVARCHAR(MAX), message_body) AS Message
FROM MyReceiveQueue
GO
-- Clean Up
USE master
GO
DROP DATABASE SBTest
GO
答案 4 :(得分:1)
我相信SQL Server确定事务中的操作不要求它锁定“正在使用”的行。我试图用更明显的东西锁定表格:
OPEN TRANSACTION
UPDATE MyQueue SET body = body
但即使这样,它仍然会返回带有READPAST提示的行。但是,如果我实际更改了行:
OPEN TRANSACTION
UPDATE MyQueue SET body = body + '.'
只有这样,带有READPAST的SELECT语句才会返回0行。似乎MSSQL足够智能,可以最大限度地减少桌面上的锁定!
我建议如果你想隐藏READPAST提示中的行,添加一个可以实际编辑的新列而不必担心更改重要数据,然后在锁定行的过程中,用实际更新该行数据变更:
ALTER TABLE MyQueue ADD LockRow bit DEFAULT(0)
...
BEGIN TRANSACTION
UPDATE MyQueue SET LockRow = 1
如果执行上述操作,则READPAST查询应返回0行。
答案 5 :(得分:1)
如果您只是需要删除所有记录而不阻止同时发生的插入和删除,您只需发出以下命令:
DELETE FROM MyQueue WITH (ROWLOCK, READPAST)
OUTPUT deleted.id, deleted.body;
这既不会阻止插入MyQueue表,也不会阻止同时执行同一语句。并发执行只会获取在上一个DELETE
事务开始时间之后插入的记录。同样,不需要执行任何ORDER BY
,因为删除主题将是在事务开始时间存在于表中的所有记录。
另外我必须提一下,我强烈建议不要使用ROWLOCK
提示,让SQL服务器决定使用哪个锁级别来提高效率。
答案 6 :(得分:1)
在选择行时,您是否尝试过发出HOLDLOCK提示?这应该确保在事务完成之前没有其他查询可以选择行:
BEGIN TRAN
DECLARE @msgCount int;
SELECT @msgCount = COUNT(*)
FROM MyQueue WITH (HOLDLOCK, ROWLOCK, READPAST);
...
COMMIT TRAN
答案 7 :(得分:1)
无法保证更改的应用顺序 到表和行插入的顺序 输出表或表变量将对应。
但是,您可以将OUTPUT
的结果插入表变量中,然后在保持排序的同时选择返回结果。请注意,如果队列中有许多记录,则使用rowlock
将需要更多资源。
DECLARE @processQueue
TABLE
( id int NOT NULL
, body nvarchar(max) /*<- set to appropriate data type!*/
);
DELETE q
OUTPUT deleted.id, deleted.body
INTO @processQueue(id, body)
FROM MyQueue q WITH (ROWLOCK, READPAST)
;
SELECT body
FROM @processQueue q
ORDER BY q.id --ensure output ordering here.
;
此外,由于您已经提到过您不想要表锁,因此可以在MyQueue
表上禁用锁升级。
--Disable lock escalation, to ensure that locks do get escalated to table locks.
ALTER TABLE MyQueue SET ( LOCK_ESCALATION = DISABLE );
go