我有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 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)
| 归档时间: |
|
| 查看次数: |
273 次 |
| 最近记录: |