Управление параллелизмом при использовании шаблона SELECT-UPDATE

25

Допустим, у вас есть следующий код (пожалуйста, не обращайте внимания, что это ужасно):

BEGIN TRAN;
DECLARE @id int
SELECT @id = id + 1 FROM TableA;
UPDATE TableA SET id = @id; --TableA must have only one row, apparently!
COMMIT TRAN;
-- @id is returned to the client or used somewhere else

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

Теперь оставьте код как есть (я понимаю, что его лучше обрабатывать как один оператор или даже лучше, используя столбец автоинкремента / идентификатора), что является верным способом заставить его обрабатывать параллелизм правильно и предотвратить гонки, которые позволяют двум клиентам получить одинаковые значение идентификатора?

Я уверен, что добавление WITH (UPDLOCK, HOLDLOCK)в SELECT поможет. Уровень изоляции транзакции SERIALIZABLE , по-видимому, также работает, поскольку он запрещает кому-либо читать то, что вы делали, до тех пор, пока не закончится транс ( ОБНОВЛЕНИЕ : это неверно. См. Ответ Мартина). Это правда? Будут ли они оба работать одинаково хорошо? Один предпочтительнее другого?

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

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

PS И нет, я не знаю лучшего ответа и действительно хочу получить лучшее понимание! :)

ErikE
источник
Просто для пояснения: хотите ли вы, чтобы 2 клиента не считывали одно и то же значение или выдавали, updateчто может быть основано на устаревших данных? В последнем случае вы можете использовать rowversionстолбец, чтобы проверить, не была ли изменена строка, подлежащая обновлению, с момента ее чтения.
a1ex07
Мы не хотим, чтобы второй клиент получал старое значение идентификатора до того, как первый клиент обновит его до нового значения. Это должно заблокировать.
ErikE

Ответы:

11

Просто обращаясь к SERIALIZABLEаспекту уровня изоляции. Да, это будет работать, но с риском тупика.

Две транзакции смогут одновременно читать строку. Они не будут блокировать друг друга, так как они будут либо брать объектную Sблокировку, либо индексную RangeS-Sблокировку, зависящую от структуры таблицы, и эти блокировки совместимы . Но они будут блокировать друг друга при попытке получить блокировки, необходимые для обновления ( IXблокировка объекта или индекс RangeS-Uсоответственно), что приведет к взаимоблокировке.

UPDLOCKВместо этого использование явной подсказки будет сериализовать чтения, что позволит избежать риска взаимоблокировки.

Мартин Смит
источник
+1, но: для таблиц кучи вы все еще можете получить тупик конверсии даже с блокировками обновлений: sqlblog.com/blogs/alexander_kuznetsov/archive/2009/03/11/…
AK
Странно, @alex. Я полагаю, что это связано с состоянием гонки двигателя, пытающегося найти, что нужно заблокировать, перед тем, как фактически ЗАБЛОКИРОВАТЬ его ...
ErikE
@ErikE - Тупик преобразования в статье Алекса является преобразование IXв Xна самой куче. Интересно, что ни одна строка не подходит, поэтому никакие блокировки строк никогда не снимаются. Не уверен, почему он берет Xблокировку вообще.
Мартин Смит
11

Я думаю, что лучшим подходом для вас было бы на самом деле выставить свой модуль на высокий уровень параллелизма и убедиться в этом самим. Иногда достаточно UPDLOCK, и нет необходимости в HOLDLOCK. Иногда sp_getapplock работает очень хорошо. Я бы не стал делать здесь никакого общего заявления - иногда добавление еще одного индекса, триггера или индексированного представления меняет результат. Нам нужно стресс-тестировать код и убедиться в этом на индивидуальной основе.

Я написал несколько примеров стресс-тестирования здесь

Изменить: для лучшего знания внутренних органов, вы можете прочитать книги Калена Делани. Однако книги могут быть не синхронизированы, как и любая другая документация. Кроме того, есть слишком много комбинаций, чтобы рассмотреть: шесть уровней изоляции, много типов блокировок, кластеризованные / некластеризованные индексы и кто знает, что еще. Это много комбинаций. Кроме того, SQL Server является закрытым исходным кодом, поэтому мы не можем загружать исходный код, отлаживать его и тому подобное - это будет основным источником знаний. Все остальное может быть неполным или устаревшим после следующего выпуска или пакета обновления.

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

Аляска
источник
9

В этом конкретном случае добавление UPDLOCKблокировки к SELECTдействительно предотвратит аномалии. Добавление HOLDLOCKне требуется, поскольку блокировка обновления удерживается на время транзакции, но я признаюсь, что сам включал ее как (возможно, плохую) привычку в прошлом.

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

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

  • Извлечение полосового запаса (<5, 10+, 50+, 100+) для продукта в интернет-магазине = грязное чтение (неточное значение не имеет значения).
  • Проверка и уменьшение уровня запаса в этом оформлении интернет-магазина = повторяемое чтение (мы ДОЛЖНЫ иметь запас до продажи, мы НЕ ДОЛЖНЫ иметь отрицательный уровень запаса).
  • Перемещение наличных денег между моим текущим и сберегательным счетами в банке = сериализуемо (не просчитывай и не ошибайся!).

Редактировать: @ комментарий Алекса Кузнецова побудил меня перечитать вопрос и убрать очевидную ошибку в моем ответе. Обратите внимание на себя в конце ночного размещения.

Марк Стори-Смит
источник