MS-SQL Server选择行,锁定行。独特的回报

时间:2011-04-16 18:06:49

标签: sql database tsql locking rowlocking

我正在通过下面的存储过程从数据库中随机选择可用的登录信息。但是当多个线程想要获取可用的登录信息时,虽然我正在更新记录的时间戳字段,但仍会返回重复的记录。

如何在此处锁定行,以便不会再次返回一次返回的记录?

  

WITH(HOLDLOCK,ROWLOCK)

没有帮助!

SELECT TOP 1 @uid = [LoginInfoUid]
      FROM [ZPer].[dbo].[LoginInfos]
      WITH (HOLDLOCK, ROWLOCK)
      WHERE ([Type] = @type)

... ... ...


ALTER PROCEDURE [dbo].[SelectRandomLoginInfo] 
    -- Add the parameters for the stored procedure here
    @type int = 0,
    @expireTimeout int = 86400 -- 24 * 60 * 60 = 24h
AS
BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON;

    -- Insert statements for procedure here
    DECLARE @processTimeout int = 10 * 60

    DECLARE @uid uniqueidentifier

    BEGIN TRANSACTION

    -- SELECT [LoginInfos] which are currently not being processed ([Timestamp] is timedout) and which are not expired.
    SELECT TOP 1 @uid = [LoginInfoUid]
      FROM [MyDb].[dbo].[LoginInfos]
      WITH (HOLDLOCK, ROWLOCK)
      WHERE ([Type] = @type) AND ([Uid] IS NOT NULL) AND ([Key] IS NOT NULL) AND
      (
        ([Timestamp] IS NULL OR DATEDIFF(second, [Timestamp], GETDATE()) > @processTimeout) OR
        (
          DATEDIFF(second, [UpdateDate], GETDATE()) <= @expireTimeout OR
          ([UpdateDate] IS NULL AND DATEDIFF(second, [CreateDate], GETDATE()) <= @expireTimeout)
        )
      )
      ORDER BY NEWID()

    -- UPDATE the selected record so that it won't be re-selected.
    UPDATE [MyDb].[dbo].[LoginInfos] SET
      [UpdateDate] = GETDATE(), [Timestamp] = GETDATE()
      WHERE [LoginInfoUid] = @uid

    -- Return the full record data.
    SELECT *
      FROM [MyDb].[dbo].[LoginInfos]
      WHERE [LoginInfoUid] = @uid

    COMMIT TRANSACTION
END

1 个答案:

答案 0 :(得分:8)

在共享模式下锁定一行对防止多个线程读取同一行没有帮助。您想要使用XLOCK提示锁定行专属。此外,您使用非常低精度的标记确定候选行(GETDATE具有3ms精度),因此您将获得大量误报。您必须使用精确的字段,例如位(processing 0或1)。

最终,您将LoginsInfo视为队列,因此建议您阅读Using tables as Queues。实现目标的方法是使用UPDATE ... WITH OUTPUT。但是您还需要选择随机登录,这会导致一切乱七八糟。你真的,真的,100%确信你需要随机性吗?这是一个非常不寻常的要求,您将很难找到一个正确且高效的解决方案。你会得到重复的,直到第二天你才会陷入僵局。

首次尝试会像:

with cte as (
 select top 1 ...
   from [LoginInfos] with (readpast)
   where processing = 0 and ...

  order by newid())
update cte
   set processing = 1
   output cte...

但是因为NEWID订单需要一个完整的表扫描并排序来挑选1个幸运的赢家行,所以你将是1)非常无形和2)不断死锁。

现在你可能会把它作为一个随机的论坛咆哮,但事实上我已经使用SQL Server支持的队列已经有好几年了,而且我知道你想要的东西是行不通的。您必须修改您的要求,特别是随机性,然后您可以返回上面链接的文章并使用其中一个真实和经过测试的方案。

修改

如果你不需要randomess,那么在某种程度上更简单。 table-as-queues问题的要点是你必须寻找你的输出行,你绝对不能扫描。扫描队列不仅没有执行,而且由于队列的使用方式而保证死锁(高度可靠的出列操作,其中所有线程都需要相同的行)。要实现这一点,您的WHERE子句必须才能实现,这取决于1)WHERE子句中的表达式和2)聚簇索引键。您的表达式不能包含OR条件,因此松散所有IS NULL OR ...,将字段修改为不可为空,始终填充它们。其次,您必须以索引方式进行比较,而不是DATEDIFF(..., field, ...) < @variable),而是始终使用field < DATEDIDD (..., @variable, ...)因为第二种形式是SARG能力。你必须选择[Timestamp][UpdateDate]两个字段中的一个,你不能同时寻求这两个字段。当然,所有这些都需要在您的应用程序中使用更加严格和严格的状态机,但这是一件好事,宽松的条件和OR条款只是数据输入不良的指示。

select @now = getdate();
select @expired = dateadd(second, @now, @processTimeout);

with cte as (
      select * 
      from [MyDb].[dbo].[LoginInfos] WITH (readpast, xlock)
      WHERE 
          [Type] = @type) AND
          [Timestamp] < @expired)
update cte
    set [Timestamp] = @now
     output INSERTED.*;

为此,表的聚簇索引必须在([Type], [Timestamp])上(这意味着使主键LoginInfoId成为非聚集索引)。