MS-SQL Server выбор строк, блокировка строк.Уникальные доходы - PullRequest
0 голосов
/ 16 апреля 2011

Я выбираю доступную информацию для входа в базу данных случайным образом с помощью хранимой процедуры ниже. Но когда несколько потоков хотят получить доступную информацию о входе в систему, возвращаются повторяющиеся записи, хотя я обновляю поле отметки времени записи.

Как я могу заблокировать здесь строки, чтобы возвращаемая однажды запись больше не возвращалась?

Ввод

С (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 Ответ

8 голосов
/ 16 апреля 2011

Блокировка строки в режиме совместного использования не помогает в том, чтобы несколько потоков не могли прочитать одну и ту же строку. Вы хотите заблокировать строку только с подсказкой XLOCK. Также вы используете маркер очень низкой точности, определяющий строки-кандидаты (GETDATE имеет точность 3 мс), поэтому вы получите много ложных срабатываний. Вы должны использовать точное поле, например, бит (processing 0 или 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...

Но поскольку для заказа NEWID требуется полное сканирование таблицы и сортировка для выбора 1 строки счастливчика, вы будете 1) крайне неработоспособным и 2) постоянно тупиком.

Теперь вы можете взять эту случайную дискуссию на форуме, но так получилось, что я уже несколько лет работаю с очередями, поддерживаемыми SQL Server, и я знаю то, что вы хотите, не будет работать. Вы должны изменить свое требование, в частности случайность, и затем вы можете вернуться к статье, указанной выше, и использовать одну из проверенных и проверенных схем.

Редактировать

Если вам не нужен случайный случай, тогда как-то проще. Суть проблемы таблиц как очередей в том, что вы должны искать выходную строку, вы абсолютно не можете сканировать для нее. Сканирование по очереди не только не выполняется, но и является гарантированной тупиковой ситуацией из-за способа использования очередей (высококонкурентные операции удаления очереди, когда все потоки хотят строку с одинаковой ). Для этого ваше предложение WHERE должно быть доступным для sarg, что зависит от 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 в некластеризованный индекс).

...