Как гарантировать целостность транзакций в SQL Server 2005 - PullRequest
2 голосов
/ 31 марта 2009

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

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

Однако под нагрузкой, когда различные серверы запускают этот хранимый процесс через ADO.NET, я обнаружил, что время от времени он возвращает один и тот же ключ разным клиентам. Это впоследствии вызывает ошибку, конечно, когда ключ используется в качестве первичного ключа!

Я наивно предполагал, что BEGIN TRAN ... COMMIT TRAN обеспечивает атомарность доступа к данным внутри области. Изучив это, я обнаружил уровни изоляции транзакций и добавил SERIALIZABLE как наиболее ограничивающий - без радости.

  Create proc [dbo].[sp_get_key] 
  @prefix nvarchar(3)
  as
  set tran isolation level SERIALIZABLE
  declare       @result nvarchar(32)  

  BEGIN TRY
      begin tran

      if (select count(*) from key_generation_table where prefix = @prefix) = 0 begin
         insert into key_generation_table (prefix, next_value) values (@prefix,1)
      end

      declare @next_value int

      select @next_value = next_value
      from key_generation_table
      where prefix = @prefix

      update key_generation_table
      set next_value = next_value + 1 
      where prefix = @prefix

      declare @string_next_value nvarchar(32)
      select @string_next_value = convert(nvarchar(32),@next_value)

      commit tran

      select @result = @prefix + substring('000000000000000000000000000000',1,10-len(@string_next_value)) + @string_next_value

      select @result

  END TRY
   BEGIN CATCH
        IF @@TRANCOUNT > 0 ROLLBACK TRAN

        DECLARE @ErrorMessage NVARCHAR(400);
        DECLARE @ErrorNumber INT;
        DECLARE @ErrorSeverity INT;
        DECLARE @ErrorState INT;
        DECLARE @ErrorLine INT;

        SELECT @ErrorMessage = N'{' + convert(nvarchar(32),ERROR_NUMBER()) + N'} ' + N'%d, Line %d, Text: ' + ERROR_MESSAGE();
        SELECT @ErrorNumber = ERROR_NUMBER();
        SELECT @ErrorSeverity = ERROR_SEVERITY();
        SELECT @ErrorState = ERROR_STATE();
        SELECT @ErrorLine = ERROR_LINE();
        RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorNumber,@ErrorLine)


    END CATCH  

Вот таблица генерации ключей ...

CREATE TABLE [dbo].[Key_Generation_Table](
        [prefix] [nvarchar](3) NOT NULL,
       [next_value] [int] NULL,
  CONSTRAINT [PK__Key_Generation_T__236943A5] PRIMARY KEY CLUSTERED 
  (
    [prefix] ASC
  )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF,      
      ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
  ) ON [PRIMARY]

Ответы [ 4 ]

1 голос
/ 31 марта 2009

Несколько вещей, которые у вас есть в состоянии гонки на вашем блоке if. Два запроса приходят на новый префикс одновременно, оба могут пройти блок if. Вы должны изменить это, чтобы всегда вставлять в таблицу, но в предложении where для вставки сделайте проверку, чтобы убедиться, что она не существует. Также я бы рекомендовал использовать Exists вместо count (*) = 0. С Exists, как только sql найдет строку, он может перестать искать.

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

Измените свою логику, чтобы сначала обновить строку, а затем получите значение, которое вы тоже обновили

   update key_generation_table          
     set next_value = next_value + 1
     where   prefix = @prefix

       select @next_value = next_value -1         
       from key_generation_table          
       where prefix = @prefix          

Я бы также посмотрел на использование оператора ouput вместо второго выбора.

EDIT

Вероятно, я бы изменил это, чтобы использовать выходные данные с yoru на SQL2005:

declare @keyTable as table  (next_value int)

UPDATE key_generation_Table
set next_value=next_value+1
OUTPUT DELETED.next_value into @keyTable(next_value)
WHERE prefix=@prefix

/* Update the following to use your formating */
select next_value from @keyTable 
1 голос
/ 31 марта 2009

Попробуйте подсказку с помощью UPDLOCK .

      select @next_value = next_value
      from key_generation_table WITH(UPDLOCK)
      where prefix = @prefix

key_generation_table в идеале используется только с этим конкретным хранимым процессом. В противном случае UPDLOCK может увеличить вероятность взаимоблокировок.

0 голосов
/ 31 марта 2009

Serializable содержит блокировки, но чтение разрешено. Таким образом, выбор / обновление в середине под нагрузкой может дать тот же результат, если процесс вызывается очень быстро параллельно. Я думаю ...

Если вы сделаете это таким образом, используя правильный синтаксис, вы можете объединить 2. Таблок гарантирует, что вся таблица заблокирована. Это отличается от сериализуемого, который является параллелизмом. Таблок это гранулярность. Кроме того, вы предполагаете, что ключ есть .. добавьте отсутствующий префикс после, если необходимо.

update
    key_generation_table WITH (TABLOCK)
set
    @next_value = next_value, next_value = next_value + 1 
where
    prefix = @prefix

if @@ROWCOUNT = 0
begin
    set @next_value  = 1
    insert into key_generation_table (prefix, next_value) values (@prefix, 1)
end
select @string_next_value = convert(nvarchar(32),@next_value)
0 голосов
/ 31 марта 2009

Подумав нестандартно, не могли бы вы просто добавить строку в таблицу с идентификатором AUTO_INCREMENT и затем использовать идентификатор? Это гарантированно будет уникальным под нагрузкой. Затем вы можете удалить строку (чтобы таблица не росла бесконечно).

Чтобы ответить на ваш вопрос о том, что происходит, транзакции не являются критическими регионами .

SERIALIZABLE

Самый строгий уровень изоляции. Когда это используется, фантомные значения не могут произойти. Он запрещает другим пользователям обновлять или вставлять строки в набор данных до завершения транзакции.

Проблема, для предотвращения которой разработан этот механизм, отличается от той, с которой вы столкнулись.

Если вы хотите использовать подход, описанный выше, вы должны получить эксклюзивную блокировку для критического региона.

...