我应该使用什么SQL Server 2005/2008锁定方法来处理多个服务器应用程序实例中的单个表行?

时间:2012-07-04 17:46:10

标签: sql-server locking

我需要开发一个服务器应用程序(在C#中),它将从一个简单的表中读取行(在SQL Server 2005或2008中),做一些工作,比如调用Web服务,然后用结果更新行状态(成功,错误)。

看起来很简单,但是当我添加以下应用程序必需品时,事情变得更加艰难:

  • 对于负载平衡和容错目的,必须同时运行多个应用程序实例。通常,应用程序将部署在两个或更多服务器上,并将同时访问同一个数据库表。 每个表行只能处理一次,因此必须在多个应用程序实例之间使用通用的同步/锁定机制。

  • 当应用程序实例处理一组行时,其他应用程序实例不必等待它结束,以便读取等待处理的另一组行。

  • 如果应用程序实例崩溃,则不需要对正在处理的表行进行手动干预(例如,删除用于在崩溃实例正在处理的行上进行应用程序锁定的临时状态)。

  • 应该以类似队列的方式处理行,即应该首先处理最旧的行。

虽然这些要求看起来并不太复杂,但我在提出解决方案方面遇到了一些麻烦。

我已经看到了锁定提示建议,例如XLOCKUPDLOCKROWLOCKREADPAST等,但我看不到锁定提示的组合允许我实现这些必要条件。

感谢您的帮助。

此致

Nuno Guerreiro

3 个答案:

答案 0 :(得分:4)

我最初为此建议了SQL Server Service Broker。然而,经过一些研究后发现这可能不是处理问题的最佳方法。

您剩下的就是您要求的桌面架构。但是,正如您所发现的那样,由于锁定,交易以及对此类方案施加的压力极大,因此您不太可能提出满足所有给定标准的解决方案。并发和每秒高事务。

注意:我目前正在研究此问题,稍后会再与您联系。以下脚本是我尝试满足给定的要求。但是,它经常出现死锁和处理项目乱序。请继续关注,同时考虑破坏性读取方法(DELETE with OUTPUT或OUTPUT INTO)。

SET XACT_ABORT ON; -- blow up the whole tran on any errors
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN TRAN

UPDATE X
SET X.StatusID = 2 -- in process
OUTPUT Inserted.*
FROM (
   SELECT TOP 1 * FROM dbo.QueueTable WITH (READPAST, ROWLOCK)
   WHERE StatusID = 1 -- ready
   ORDER BY QueuedDate, QueueID -- in case of items with the same date
) X;

-- Do work in application, holding open the tran.

DELETE dbo.QueueTable WHERE QueueID = @QueueID; -- value taken from recordset that was output earlier

COMMIT TRAN;

如果单个客户端同时锁定了多个/多个行,则行锁可能会升级到扩展区,页面或表锁,因此请注意这一点。此外,通常持有长时间运行的事务来保持锁定是一个很大的禁忌。它可能适用于这种特殊用例,但我担心多个客户端的高tps会使系统崩溃。请注意,通常,查询队列表的唯一进程应该是正在执行队列工作的进程。任何进行报告的进程都应该使用READ UNCOMMITTED或WITH NOLOCK来避免以任何方式干扰队列。

无序处理行的含义是什么?如果应用程序实例在另一个实例成功完成行时崩溃,则此延迟可能会导致至少一行在其完成时延迟,从而导致处理顺序不正确。

如果上面的事务/锁定方法不满意,处理应用程序崩溃的另一种方法是提供实例名称,然后设置一个监视进程,该进程能够定期检查这些命名实例是否正在运行。当命名实例启动时,它将始终重置拥有其实例标识符的任何未处理的行(像“实例A”和“实例B”那样简单的工作)。此外,监视器进程将检查实例是否正在运行,如果其中一个实例未运行,请重置该缺失实例的行,以启用任何其他实例。崩溃和恢复之间会有一个小的延迟,但如果采用适当的架构,这可能是非常合理的。

注意:以下链接应该有所启发:

答案 1 :(得分:4)

这是典型的表格作为队列模式,如Using tables as Queues中所述。您将使用Pending Queue,并且dequeue事务还应该在合理的超时中安排重试。实际上不可能在Web调用期间保持锁定。成功时,您将删除待处理的项目。

您还需要能够批量出队,如果您进入严重负载(每秒100次和数千次操作),则逐个出列太慢。所以从链接的文章中获取Pending Queue示例:

create table PendingQueue (
  id int not null,
  DueTime datetime not null,
  Payload varbinary(max),
  cnstraint pk_pending_id nonclustered primary key(id));

create clustered index cdxPendingQueue on PendingQueue (DueTime);
go

create procedure usp_enqueuePending
  @dueTime datetime,
  @payload varbinary(max)
as
  set nocount on;
  insert into PendingQueue (DueTime, Payload)
    values (@dueTime, @payload);
go

create procedure usp_dequeuePending
  @batchsize int = 100,
  @retryseconds int = 600
as
  set nocount on;
  declare @now datetime;
  set @now = getutcdate();
  with cte as (
    select top(@batchsize) 
      id,
      DueTime,
      Payload
    from PendingQueue with (rowlock, readpast)
    where DueTime < @now
    order by DueTime)
  update cte
    set DueTime = dateadd(seconds, @retryseconds, DueTime)
    output deleted.Payload, deleted.id;
go

成功处理后,您将使用ID从队列中删除该项目。失败或崩溃时,它将在10分钟后自动重试。有人认为你必须内化的是,只要HTTP不提供事务语义,你就永远不会能够以100%一致的语义来做到这一点(例如,保证没有项目被处理两次)。您可以获得非常高的错误余量,但在数据库更新之前,在HTTP调用成功之后,系统总会出现崩溃,并且会导致重试相同的项目,因为您无法将此案例与系统在 HTTP调用之前崩溃的情况。

答案 2 :(得分:2)

您不能使用SQL事务(或在此处依赖事务作为主要组件)来执行此操作。实际上,你可以这样做,但你不应该这样做。对于长锁,交易不应该以这种方式使用,你不应该像这样滥用它们。

长时间保持事务处理(检索行,调用Web服务,返回进行更新)根本就不好。而且没有optimistic locking隔离级别可以让你做你想做的事。

使用ROWLOCK也不是一个好主意,因为它就是这样。 提示。它受lock escalation的约束,可以转换为表锁。

我可以建议您的数据库有一个入口点吗?我认为它适合于pub / sub设计。 因此,只有一个组件可以读取/更新这些记录:

  1. 读取批量消息(足以让所有其他实例消耗) - 1000,10000,无论你认为合适。它通过一些排队的方式使这些批次可用于其他(并发)组件。我不会说MSMQ :)(这是我今天第二次推荐它,但它也非常适合你的情况)。
  2. 它将消息标记为in progress或类似的内容。
  3. 您的消费者都在事务上绑定到入站队列并完成他们的工作。
  4. 准备好后,在Web服务调用之后,他们将消息放入出站队列。
  5. 中央组件选择它们,并且在分布式事务中,对数据库进行更新(如果失败,则消息将保留在队列中)。因为它是唯一可以执行该操作的人,所以不会出现任何并发问题。至少不在数据库上。
  6. 同时它可以读取下一个待处理批次,依此类推。