Вопрос о состоянии гонки 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 и документы.
резюме:
- вы начали транзакцию. Это на самом деле ничего не" делает " само по себе, оно изменяет последующее поведение
- Вы читаете данные из таблицы. Уровень изоляции по умолчанию-Read Committed, поэтому эта инструкция select -не частью сделки.
- вы тогда подождите 15 секунд
- затем вы выпускаете обновление. С объявленной транзакцией это создаст блокировку до тех пор, пока транзакция не будет привержен.
- затем вы совершаете транзакцию, освобождая блокировку.
Итак, предполагая, что вы запустили это одновременно в двух окнах (A и B):
- a прочитайте значение "next" из таблицы def, затем перейдите в режим ожидания
- B прочитал то же самое" следующее " значение из таблицы, затем перешел в режим ожидания. (Поскольку A только сделал чтение, транзакция ничего не заблокировала.)
- A затем обновил таблицу и, вероятно, внес изменения до Б вышел из состояния ожидания.
- B затем обновил таблицу после того, как запись A была зафиксирована.
Попробуйте поместить оператор wait после обновления, перед фиксацией, и посмотреть, что произойдет.
Это не настоящее состояние гонки. Это более распространенная проблема с параллельными транзакциями. Одно из решений-установить блокировку чтения на таблице и для этого иметь сериализацию на месте.
Это на самом деле распространенная проблема в базах данных SQL, и именно поэтому большинство (все?) из них имеют некоторые встроенные функции, чтобы позаботиться об этом вопросе получения уникального идентификатора. Вот некоторые вещи, чтобы посмотреть, если вы используете Mysql или Postgres. Если вы используете другую базу данных, я уверен, дают что-то очень похожее.
хорошим примером этого являются последовательности postgres, которые вы можете проверить здесь:
Mysql использует что-то под названием auto increments.
можно задать для столбца вычисляемое значение, которое сохраняется. Это позаботится о состоянии гонки.
Материализованные Вычисляемые Столбцы
Примечание
использование этого метода означает, что вам не нужно хранить следующий код в таблице. Столбец кода становится точкой отсчета.
реализация
дайте столбцу следующие свойства в вычисляемом столбце спецификация.
Формула = 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