如何以事务方式从两个表中选择记录

Sae*_*ati 5 sql-server

我有Messages一个包含字段的表和另一个名为 的表SentMessages,它与该表具有一对一的关系Messages

Messages 存储有关消息的信息,包括其文本、主题和收件人等。

create table Messages
(
    Id bigint not null identity(1, 1) primary key,
    Text nvarchar(max),
    Subject nvarchar(400)
    -- other fields
)
Run Code Online (Sandbox Code Playgroud)

SentMessages 存储与将消息传输到客户端相关的信息。

create table SentMessages
(
    Id bigint not null primary key,
    SentOn datetime not null,
    DeliveredOn datetime null,
    -- other fields
)
go
alter table SentMessages with check add constrait [FK_SentMessages_Messages] 
foreign key([Id])
references Messages ([Id])
Run Code Online (Sandbox Code Playgroud)

它们被设计为独立的,所以请不要对合并它们提出建议。

现在的逻辑是找到所有尚未发送的消息,然后发送它们。基本上not in,单线程场景中的一个简单子句就可以解决问题:

select *
from Messages
where Id not in 
(
    select Id 
    from SentMessages
)
Run Code Online (Sandbox Code Playgroud)

但是这个数据库被许多并发线程使用,因此一条消息可能被多个线程占用,因此被发送两次甚至更多。

如果它们是单个表,我可以使用update select语句在单个事务中选择未发送的消息。

我有哪些选项可以使选择具有事务性,以便消息不会被发送两次?

SQL*_*tor 4

你不能只得到蛋糕却把它整个留下;一方面,您希望允许最大并发性,另一方面,您希望每个进程都是“隔离的”并且对其他进程不可见。SQL Server 提供的Service Broker正是针对这些排队/异步处理挑战。看看吧,这是一个很棒的基础设施,很可能比任何自定义用户应用程序解决方案都工作得更好。

在我们拥有 Service Broker 之前,我们用来应对类似挑战的方法是创建第三个表,将其称为“电子邮件发送队列”或类似的名称。让它只保存需要发送的消息的 ID,因此每次将其输入“消息”时,也将 ID 输入到队列中。

当进程需要选择要发送的消息时,您可以在 REPEATABLE READ 隔离级别中打开一个显式事务,并将其保持打开状态,直到发送进程完成。每个需要选择工作的新流程都使用

SELECT TOP 1 ID FROM EmailSendQueue WITH (READPAST) ORDER BY ID ASC
Run Code Online (Sandbox Code Playgroud)

这样,读取过程将简单地跳过锁定的行,并获取下一个尚未锁定的行,即尚未处理的行。即使您能够(顺便说一句,您可以......)在原始表上持有显式锁,而无需 READPAST 技巧,这将是阻塞地狱的秘诀。这是一个简单的、未经测试的示例代码来实现这一目的。

CREATE TABLE [EmailSendQueue]
(
[MessageID] BIGINT NOT NULL 
-- Better use a heap here to prevent page level resrouce contention 
PRIMARY KEY NONCLUSTERED
REFERENCES [Messages] (ID)
-- In case someone deletes a message that has not been sent yet, 
-- and to prevent deleting a message which is in process
ON DELETE CASCADE 
ON UPDATE NO ACTION
);

-- New messages into queue when ready to send - allows delayed send logic as well...
INSERT INTO [EmailSendQueue]([MessageID]) 
VALUES (1), (2), (3);

-- First Process picks up #1 
-- Second process will pick up #2, if #1 is still locked, or is completed...
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION;

DECLARE @MessageID BIGINT;

SELECT TOP (1) @MessageID = [MessageID] 
FROM [EmailSendQueue] WITH (READPAST, XLOCK)
ORDER BY [MessageID] ASC;

DECLARE @StartTime DATETIME = GETDATE();

-- Process and send message
EXECUTE [SendMessageProcedure] @MessageID; 

-- Send successful
IF @@Error = 0
BEGIN
-- Now the message has been sent, and can go into SentMessages
    INSERT INTO [SentMessages] ([ID], [SendOn], [DeliveredOn])
    VALUES (@MessageID, @StartTime, GETDATE());
    DELETE FROM [EmailSendQueue] WHERE [MessageID] = @MessageID
END;

-- If send unsuccesful, you can do nothing, or introduce some kind of retry logic
-- You can add a 'Retry' column to the queue, and increment it on every attempt 
-- A scheduled backgrond process can delete all messages that  exceed a retry threshold
-- This will prevent 'poisoning' the queue with error messages

COMMIT TRANSACTION;
Run Code Online (Sandbox Code Playgroud)