“签出”记录以进行处理的策略


10

我不确定是否有一个命名模式,或者没有,因为这是一个糟糕的主意。但是我需要我的服务才能在主动/主动负载平衡环境中运行。这仅是应用服务器。该数据库将位于单独的服务器上。我有一项服务,该服务将需要为表中的每个记录运行一个过程。此过程可能需要一到两分钟,并且每n分钟重复一次(可配置,通常为15分钟)。

拥有需要处理的1000条记录的表,以及针对同一数据集运行的两个服务,我希望每个服务“签出”要处理的记录。我需要确保一次只有一个服务/线程正在处理每个记录。

我的同事过去曾经使用过“锁定表”。将记录写入此表以在逻辑上锁定另一个表中的记录(该另一个表是静态的,并且添加了非常偶尔的新记录),然后将其删除以释放锁定。

我想知道对于新表来说,是否有更好的一列来表明它何时被锁定以及当前是否被锁定,而不是不断插入一个删除项,这会更好。

有人对这种事情有提示吗?是否有建立长期(逻辑)逻辑锁定的模式?如何确保一次仅锁定一项服务的任何提示?(我的同事使用TABLOCKX锁定整个表。)

Answers:


12

我既不喜欢额外的“锁定”表,也不喜欢锁定整个表以获取下一个记录。我知道为什么这样做了,但是这也损害了正在更新以释放锁定记录的操作的并发性(当两个进程不可能在同一时刻将同一记录锁定时,两个进程肯定无法抗衡。同时)。

我的偏好是向表中添加一个ProcessStatusID(通常为TINYINT)列,其中包含正在处理的数据。是否有LastModifiedDate的字段?如果不是,则应添加它。如果是,那么这些记录是否在此处理之外得到更新?如果可以在此特定过程之外更新记录,则应添加另一个字段以跟踪StatusModifiedDate(或类似的内容)。对于此答案的其余部分,我将仅使用“ StatusModifiedDate”,因为其含义很明显(实际上,即使当前没有“ LastModifiedDate”字段,也可以将其用作字段名称)。

ProcessStatusID的值(应将其放入名为“ ProcessStatus”的新查找表中,并在此表中使用外键):

  1. 已完成(在这种情况下,甚至是“待定”,因为两者均表示“已准备就绪”)
  2. 处理中(或“处理中”)
  3. 错误(或“ 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_getapplocksp_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仅在行级别或页面级别为读取操作获取更新锁。


1

使用Service Broker队列做了类似的事情(没有应用程序,仅在DB内)。完全兼容ACID的轻量级产品几乎可以无限扩展。内置了透明的行锁定(或“隐藏”)。从版本2005开始可用。

在您的情况下,整个体系结构可能是这样的:某些进程根据消息的日程安排将消息发送到Service Broker对话框,然后侦听器从目标端的队列中提取消息。除了创建单独的消息类型之外,您还可以在消息正文中包含几乎所有内容,例如超时以及任务可能具有的任何参数。

当然,这不是一件最容易掌握的事情,但是一旦您掌握了它,它的优势就会显而易见。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.