0

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

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

推杆

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
4

1 回答 1

8

在共享模式下锁定一行对于防止多个线程读取同一行没有一点帮助。您想用XLOCK提示独占锁定行。此外,您正在使用一个非常低精度的标记来确定候选行(GETDATE精度为 3 毫秒),因此您会得到很多误报。您必须使用精确字段,例如位(processing0 或 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 支持的队列多年了,我知道您想要的不会起作用。您必须修改您的要求,特别是随机性,然后您可以返回上面链接的文章并使用真实且经过测试的方案之一。

编辑

如果您不需要随机性,那么在某种程度上更简单。表即队列问题的要点是你必须寻找你的输出行,你绝对不能扫描它。扫描队列不仅没有执行,而且由于使用队列的方式(高度并发的出列操作,所有线程都想要相同的行),肯定会出现死锁。要实现这一点,您的 WHERE 子句必须是 sarg-able,这取决于 1) 您在 WHERE 子句中的表达式和 2) 聚集索引键。您的表达式不能包含OR条件,因此请松开所有IS NULL OR ...,将字段修改为不可为空并始终填充它们。其次,您必须以友好的方式比较索引,而不是DATEDIFF(..., field, ...) < @variable)但总是使用field < DATEDIDD (..., @variable, ...),因为第二种形式是 SARG-able。你必须满足于两个领域之一,[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非聚集索引)。

于 2011-04-16T18:51:24.477 回答