Вопрос о состоянии гонки SQL Server

(Примечание: это для MS SQL Server)

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

скажем, у вас есть другая таблица DEF только с одной строкой, в которой хранится следующий доступный код (представьте себе простой autonumber).

Я знаю, что логика, как показано ниже, представит состояние гонки, в котором два пользователя могут закончиться с тем же кодом:

1) Run a select query to grab next available code from DEF
2) Insert said code into table ABC
3) Increment the value in DEF so it's not re-used.

Я знаю, что два пользователя могут застрять на Шаге 1) и могут получить тот же код в таблице ABC.

каков наилучший способ справиться с этой ситуацией? Я думал, что могу просто обернуть "begin tran" / "commit tran" вокруг этой логики, но я не думаю, что это сработало. У меня была такая хранимая процедура для тестирования, но я не избежал состояния гонки, когда я бежал из двух разных окон в MS:

begin tran

declare @x int

select   @x= nextcode FROM  def

waitfor delay '00:00:15'

update def set nextcode = nextcode + 1

select @x

commit tran

кто-то может пролить свет на это? Я думал, что транзакция помешает другому пользователю получить доступ к моему NextCodeTable до завершения первой транзакции, но я думаю, что мое понимание транзакций ущербно.

EDIT: я попытался переместить ожидание после оператора "update", и я получил два разных кода... но я подозревал это. У меня есть заявление waitfor, чтобы имитировать задержку, чтобы можно было легко увидеть состояние гонки. Я думаю, что ключевая проблема - моя неправильная восприятие как сделки.

7 ответов


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

просмотрите сведения об уровнях изоляции SQL Server здесь

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

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

begin tran
declare @x int
update def set @x= nextcode, nextcode += 1
waitfor delay '00:00:15'
select @x
commit tran

Как упоминали другие респонденты, вы можете установить уровень изоляции транзакции, чтобы гарантировать, что все, что Вы "читаете" с помощью инструкции SELECT, не может измениться в транзакции.

кроме того, вы можете вынуть блокировку специально для таблицы DEF, добавив синтаксис WITH HOLDLOCK после имени таблицы, например,

SELECT nextcode FROM DEF WITH HOLDLOCK

это не имеет большого значения здесь, так как ваша транзакция мала, но может быть полезно вынуть замки для некоторых выбирает, а не другие в рамках транзакции. Это вопрос "повторяемости и параллелизма".

пару соответствующих МС-SQL и документы.


поздний ответ. Вы хотите избежать состояния гонки...

"условие гонки очереди процесса SQL Server"


резюме:

  • вы начали транзакцию. Это на самом деле ничего не" делает " само по себе, оно изменяет последующее поведение
  • Вы читаете данные из таблицы. Уровень изоляции по умолчанию-Read Committed, поэтому эта инструкция select -не частью сделки.
  • вы тогда подождите 15 секунд
  • затем вы выпускаете обновление. С объявленной транзакцией это создаст блокировку до тех пор, пока транзакция не будет привержен.
  • затем вы совершаете транзакцию, освобождая блокировку.

Итак, предполагая, что вы запустили это одновременно в двух окнах (A и B):

  • a прочитайте значение "next" из таблицы def, затем перейдите в режим ожидания
  • B прочитал то же самое" следующее " значение из таблицы, затем перешел в режим ожидания. (Поскольку A только сделал чтение, транзакция ничего не заблокировала.)
  • A затем обновил таблицу и, вероятно, внес изменения до Б вышел из состояния ожидания.
  • B затем обновил таблицу после того, как запись A была зафиксирована.

Попробуйте поместить оператор wait после обновления, перед фиксацией, и посмотреть, что произойдет.


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


Это на самом деле распространенная проблема в базах данных SQL, и именно поэтому большинство (все?) из них имеют некоторые встроенные функции, чтобы позаботиться об этом вопросе получения уникального идентификатора. Вот некоторые вещи, чтобы посмотреть, если вы используете Mysql или Postgres. Если вы используете другую базу данных, я уверен, дают что-то очень похожее.

хорошим примером этого являются последовательности postgres, которые вы можете проверить здесь:

Postgres Последовательности

Mysql использует что-то под названием auto increments.

автоматическое увеличение Mysql


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

Материализованные Вычисляемые Столбцы

Примечание

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

реализация

дайте столбцу следующие свойства в вычисляемом столбце спецификация.

Формула = dbo.GetNextCode ()

Сохраняется = Yes

Create Function dbo.GetNextCode()
Returns VarChar(10)
As
Begin

    Declare @Return VarChar(10);
    Declare @MaxId Int

    Select @MaxId = Max(Id)
    From Table

    Select @Return = Code
    From Table
    Where Id = @MaxId;

    /* Generate New Code ... */

    Return @Return;

End