我既不喜欢额外的“锁定”表,也不喜欢锁定整个表以获取下一个记录。我知道为什么这样做了,但是这也损害了正在更新以释放锁定记录的操作的并发性(当两个进程不可能在同一时刻将同一记录锁定时,两个进程肯定无法抗衡。同时)。
我的偏好是向表中添加一个ProcessStatusID(通常为TINYINT)列,其中包含正在处理的数据。是否有LastModifiedDate的字段?如果不是,则应添加它。如果是,那么这些记录是否在此处理之外得到更新?如果可以在此特定过程之外更新记录,则应添加另一个字段以跟踪StatusModifiedDate(或类似的内容)。对于此答案的其余部分,我将仅使用“ StatusModifiedDate”,因为其含义很明显(实际上,即使当前没有“ LastModifiedDate”字段,也可以将其用作字段名称)。
ProcessStatusID的值(应将其放入名为“ ProcessStatus”的新查找表中,并在此表中使用外键):
- 已完成(在这种情况下,甚至是“待定”,因为两者均表示“已准备就绪”)
- 处理中(或“处理中”)
- 错误(或“ WTF?”)
在这一点上,可以断定从应用程序开始,它只是想获取下一条要处理的记录,而不会传递任何信息来帮助做出该决定。因此,我们想获取设置为“已完成” /“待处理”的最旧的记录(至少就StatusModifiedDate而言)。类似于以下内容:
SELECT TOP 1 pt.RecordID
FROM ProcessTable pt
WHERE pt.StatusID = 1
ORDER BY pt.StatusModifiedDate ASC;
我们还希望同时将该记录更新为“正在处理”,以防止其他进程抓住它。我们可以使用该OUTPUT
子句让我们在同一事务中执行UPDATE和SELECT:
UPDATE TOP (1) pt
SET pt.StatusID = 2,
pt.StatusModifiedDate = GETDATE() -- or GETUTCDATE()
OUTPUT INSERTED.RecordID
FROM ProcessTable pt
WHERE pt.StatusID = 1;
这里的主要问题是,尽管我们可以TOP (1)
在一个UPDATE
操作中进行操作,但无法进行操作ORDER BY
。但是,我们可以将其包装在CTE中以结合这两个概念:
;WITH cte AS
(
SELECT TOP 1 pt.RecordID
FROM ProcessTable pt (READPAST, ROWLOCK, UPDLOCK)
WHERE pt.StatusID = 1
ORDER BY pt.StatusModifiedDate ASC;
)
UPDATE cte
SET cte.StatusID = 2,
cte.StatusModifiedDate = GETDATE() -- or GETUTCDATE()
OUTPUT INSERTED.RecordID;
显而易见的问题是,同时执行SELECT的两个进程是否可以获取相同的记录。我非常确定UPDATE with OUTPUT子句,尤其是与READPAST和UPDLOCK提示结合使用(请参阅下文以了解更多详细信息)会很好。但是,我没有测试这种确切的情况。如果由于某种原因上述查询不能解决竞争条件,则添加以下内容:应用程序锁定。
可以将上述CTE查询包装在sp_getapplock和sp_releaseapplock中,以为该进程创建一个“关守 ”。这样做时,一次只能输入一个进程才能运行上面的查询。其他进程将被阻止,直到带有applock的进程释放它为止。并且由于整个过程的这一步只是获取RecordID,因此它相当快,并且不会长时间阻塞其他过程。并且,就像CTE查询一样,我们不会阻塞整个表,从而允许对其他行进行其他更新(将其状态设置为“已完成”或“错误”)。实质上:
BEGIN TRANSACTION;
EXEC sp_getapplock @Resource = 'GetNextRecordToProcess', @LockMode = 'Exclusive';
{CTE UPDATE query shown above}
EXEC sp_releaseapplock @Resource = 'GetNextRecordToProcess';
COMMIT TRANSACTION;
应用程序锁非常好,但应谨慎使用。
最后,您只需要一个存储过程即可将状态设置为“已完成”或“错误”。那可能很简单:
CREATE PROCEDURE ProcessTable_SetProcessStatusID
(
@RecordID INT,
@ProcessStatusID TINYINT
)
AS
SET NOCOUNT ON;
UPDATE pt
SET pt.ProcessStatusID = @ProcessStatusID,
pt.StatusModifiedDate = GETDATE() -- or GETUTCDATE()
FROM ProcessTable pt
WHERE pt.RecordID = @RecordID;
表提示(可在提示(Transact-SQL)-表中找到):
READPAST(似乎适合此确切方案)
指定数据库引擎不读取其他事务锁定的行。指定READPAST时,将跳过行级锁。也就是说,数据库引擎跳过行而不是阻塞当前事务,直到释放锁为止... READPAST主要用于在实现使用SQL Server表的工作队列时减少锁争用。使用READPAST的队列读取器将过去由其他事务锁定的队列条目跳过到下一个可用队列条目,而不必等到其他事务释放其锁。
ROWLOCK(为了安全起见)
指定当通常采用页锁或表锁时采用行锁。
密码锁
指定将获取并保持更新锁,直到事务完成。UPDLOCK仅在行级别或页面级别为读取操作获取更新锁。