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

Sam*_* S. 0 sql t-sql database locking rowlocking

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

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

WITH(HOLDLOCK,ROWLOCK)

没有帮助!

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

...... ......


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
Run Code Online (Sandbox Code Playgroud)

Rem*_*anu 8

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

最终你将其LoginsInfo视为一个队列,所以我建议你阅读使用表作为队列.实现你想要的方法是使用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...
Run Code Online (Sandbox Code Playgroud)

但是因为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.*;
Run Code Online (Sandbox Code Playgroud)

为此,必须启用表的聚簇索引([Type], [Timestamp])(这意味着使主键LoginInfoId成为非聚簇索引).