Зачем нужны подсказки блокировки в атомарном операторе?

Вопрос

В чем преимущество применения блокировок к приведенному ниже утверждению?

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

NB: это переполнение из заданного здесь вопроса: SQL Threadsafe UPDATE TOP 1 для очереди FIFO

Заявление в вопросе

WITH nextRecordToProcess AS
(
    SELECT TOP(1) Id, StatusId
    FROM    DemoQueue
    WHERE   StatusId = 1 --Ready for processing
    ORDER BY DateSubmitted, Id 
)
UPDATE nextRecordToProcess
SET StatusId = 2 --Processing
OUTPUT Inserted.Id 

Требование

  • SQL используется для извлечения необработанной записи из очереди.
  • Получаемая запись должна быть первой записью в очереди со статусом Ready (StatusId = 1).
  • Может быть несколько рабочих/сеансов, обрабатывающих сообщения из этой очереди.
  • Мы хотим гарантировать, что каждая запись в очереди извлекается только один раз (т. е. одним рабочим процессом) и что каждый рабочий процесс обрабатывает сообщения в том порядке, в котором они появляются в очереди.
  • Это нормально, если один работник работает быстрее, чем другой (т. е. если рабочий A извлекает запись 1, затем рабочий B извлекает запись 2, это нормально, если рабочий B завершает обработку записи 2 до того, как рабочий A завершает обработку записи 1). Мы заинтересованы только в контексте получения записи.
  • Нет текущей транзакции; т.е. мы просто хотим забрать запись из очереди; нам не нужно держать его заблокированным, пока мы не вернемся, чтобы изменить статус с Processing на Processed.

Дополнительный SQL для контекста:

CREATE TABLE Statuses
(
    Id SMALLINT NOT NULL PRIMARY KEY CLUSTERED
    , Name NVARCHAR(32) NOT NULL UNIQUE
)
GO
INSERT Statuses (Id, Name)
VALUES (0,'Draft')
, (1,'Ready')
, (2,'Processing')
, (3,'Processed')
, (4,'Error')
GO
CREATE TABLE DemoQueue
(
    Id BIGINT NOT NULL IDENTITY(1,1) PRIMARY KEY CLUSTERED
    , StatusId SMALLINT NOT NULL FOREIGN KEY REFERENCES Statuses(Id)
    , DateSubmitted DATETIME --will be null for all records with status 'Draft'
)
GO

Предлагаемое заявление

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

WITH nextRecordToProcess AS
(
    SELECT TOP(1) Id, StatusId
    FROM    DemoQueue WITH (UPDLOCK, ROWLOCK, READPAST)
    WHERE   StatusId = 1 --Ready for processing
    ORDER BY DateSubmitted, Id 
)
UPDATE nextRecordToProcess
SET StatusId = 2 --Processing
OUTPUT Inserted.Id 

Мое понимание

Я понимаю, что если бы потребовалась блокировка, преимущества этих подсказок были бы:

  • UPDLOCK: Поскольку мы выбираем запись для обновления ее статуса, нам необходимо убедиться, что любые другие сеансы, читающие эту запись после того, как мы ее прочитали, но до того, как мы ее обновили, не смогут прочитать запись с намерением обновить его (точнее, такой оператор должен был бы ждать, пока мы не выполним наше обновление и не снимем блокировку, прежде чем другой сеанс сможет увидеть нашу запись с ее новым значением).
  • ROWLOCK: пока мы блокируем запись, мы хотим убедиться, что наша блокировка влияет только на блокируемую нами строку; т.е. поскольку нам не нужно блокировать много ресурсов / мы не хотим влиять на другие процессы / мы хотим, чтобы другие сеансы могли читать следующий доступный элемент в очереди, даже если этот элемент находится на той же странице, что и наша заблокированная запись .
  • READPAST: если другой сеанс уже читает элемент из очереди, вместо того, чтобы ждать, пока этот сеанс освободит свою блокировку, наш сеанс должен выбрать следующую доступную (не заблокированную) запись в очереди.

т. е. если бы мы запускали приведенный ниже код, я думаю, это имело бы смысл:

DECLARE @nextRecordToProcess BIGINT

BEGIN TRANSACTION

SELECT TOP (1) @nextRecordToProcess = Id
FROM    DemoQueue WITH (UPDLOCK, ROWLOCK, READPAST)
WHERE   StatusId = 1 --Ready for processing
ORDER BY DateSubmitted, Id 

--and then in a separate statement

UPDATE DemoQueue
SET StatusId = 2 --Processing
WHERE Id = @nextRecordToProcess

COMMIT TRANSACTION

--@nextRecordToProcess is then returned either as an out parameter or by including a `select @nextRecordToProcess Id`

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

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


person JohnLBevan    schedule 17.03.2018    source источник
comment
Использование таблиц в качестве очередей   -  person Lukasz Szozda    schedule 18.03.2018
comment
Спасибо @lad2025; Я прочитал эту статью в другой ветке, но она меня не просветила; т. е. упоминается, что мы получаем блокировку LCK_M_U, но не говорится, зачем нам это нужно/в чем была бы проблема, если бы мы не беспокоились о табличных подсказках. Меня беспокоит то, что любой, кто не понимает, зачем нужны эти подсказки, может начать добавлять подсказки везде, просто на всякий случай; а использование намеков без понимания и обоснования — плохой сценарий. В исходном потоке я даже набросал код, чтобы попытаться вызвать состояние гонки; но не видел ни одного / даже не знаю, то ли это то, чего мы стремимся избежать   -  person JohnLBevan    schedule 18.03.2018
comment
пс. Только что обнаружил, что автор блога находится на SO (или, по крайней мере, кто-то с очень похожим именем пользователя, который ссылается на это сообщение в блоге). stackoverflow.com/a/3643282/361842. Думаю, теперь я понимаю; он говорит о наличии специальной таблицы для очередей для высокопроизводительных сценариев. Вопрос Кирстен (связанный) касается использования обычной таблицы Invoices и имитации очереди поверх нее с подключением нескольких бизнес-систем. т. е. совет, данный в блоге, предназначен для другого сценария.   -  person JohnLBevan    schedule 18.03.2018


Ответы (2)


Джон прав, так как это оптимизация, но в мире SQL эти оптимизации могут означать разницу между «быстро» и «невероятно медленным размером данных» и/или разницу между «работает» и «непригодным тупиковым беспорядком». .

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

  • Подсказка ROWLOCK состоит в том, чтобы предотвратить детальное сканирование блокировки страницы. Степень детализации блокировки (строка или страница) определяется заранее, когда запускается запрос, и основана на оценке количества страниц, которые будет сканировать запрос (третья степень детализации, таблица, будет использоваться только в особых случаях и здесь не применяется). ). Обычно операции удаления из очереди никогда не должны сканировать такое количество страниц, чтобы ядро ​​учитывало степень детализации страниц. Но я видел «в дикой природе» случаи, когда движок решил использовать гранулярность блокировки страниц, и это приводило к блокировке и взаимоблокировкам при удалении из очереди.
  • UPDLOCK необходим для предотвращения сценария взаимоблокировки блокировки обновления. Оператор UPDATE логически разделен на поиск строк, которые необходимо обновить, и последующее обновление строк. Поиск должен заблокировать строки, которые он оценивает. Если строка соответствует требованиям (соответствует условию WHERE), то строка обновляется, а обновление всегда является монопольной блокировкой. Итак, вопрос в том, как вы блокируете строки во время поиска? Если вы используете общую блокировку, то два UPDATE будут просматривать одну и ту же строку (они могут, поскольку общая блокировка позволяет им), оба решают, что строка соответствует требованиям, и оба пытаются обновить блокировку до монопольной -> тупиковой. Если вы используете эксклюзивные блокировки во время поиска, взаимоблокировка не может произойти, но тогда UPDATE будет конфликтовать со всеми строками, оцениваемыми с любым другим чтением, даже если строка не соответствует требованиям (не говоря уже о том, что эксклюзивные блокировки не могут быть освобождены раньше без нарушения двухфазная блокировка). Вот почему существует блокировка режима U, совместимая с Shared (чтобы оценка UPDATE строк-кандидатов не блокировала чтение), но несовместимая с другим U (чтобы два UPDATE не блокировались). Есть две причины, по которым эта подсказка требуется типичному исключению из очереди на основе CTE:

    1. because is a CTE the query processing does not understand always that the SELECT inside the CTE is the target of an UPDATE and should use U mode locks and
    2. операция удаления из очереди всегда будет выполняться после одних и тех же строк для обновления (строки «изымаются из очереди»), поэтому взаимоблокировки возникают часто.
person Remus Rusanu    schedule 19.03.2018
comment
Спасибо @RemusRusanu. Я думаю, что мое понимание слова ATOMic было немного неправильным; Я понимал, что это означает «все или ничего», но я наделил его волшебными способностями в том, как он достиг этого, и предположил, что SQL Engine лучше понимает мои намерения для более сложных однооператорных запросов. - person JohnLBevan; 19.03.2018

tl;dr

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

Подробный

Я думаю, что нашел ответ, найдя связанный ответ SO от цитируемый автор этого блога.

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

  • Блокировка ReadPast повышает производительность в сценариях с высокой степенью параллелизма; нет ожидания освобождения текущей читаемой записи; единственное, что заблокирует его, — это другой процесс «Рабочий очереди», поэтому мы можем спокойно пропустить, зная, что этот рабочий процесс имеет дело с этой записью.
  • RowLock гарантирует, что мы не блокируем более одной строки за раз, поэтому следующий рабочий процесс, который запросит сообщение, получит следующую запись, а не пропустит несколько записей, потому что они находятся на странице заблокированной записи.
  • UpdLock используется для блокировки; то есть RowLock говорит, что блокировать, но не говорит, что блокировка должна быть, а ReadPast определяет поведение при обнаружении других заблокированных записей, поэтому опять же не вызывает блокировку текущей записи. Я подозреваю, что это явно не требуется, поскольку SQL все равно получит его в фоновом режиме (фактически, в связанном ответе SO указано только ReadPast); но был включен в сообщение блока для полноты / чтобы явно показать блокировку, которую SQL в любом случае будет неявно вызывать в фоновом режиме.

Однако это сообщение написано для специальной таблицы очереди. Где таблица используется для других целей (например, в исходном вопросе это была таблица, содержащая данные счета, в которой имелся столбец, используемый для отслеживания того, что было напечатано), этот совет может быть нежелательным. то есть, используя блокировку ReadPast, вы перескакиваете через все заблокированные записи; и нет никакой гарантии, что эти записи будут заблокированы другим исполнителем, обрабатывающим вашу очередь; они могут быть заблокированы для каких-то совершенно не связанных целей. Это нарушит требование FIFO.

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

person JohnLBevan    schedule 17.03.2018
comment
повысить производительность в сценариях с высоким уровнем параллелизма ... это понятно, но наверняка UpdLock есть ли для ATOM не производительность? Насколько я понимаю, это для предотвращения другого читателя между SELECT-частью и UPDATE-частью. Если у меня есть два или более процессов, всегда будет риск того, что у них будет гонка, даже если вероятность невероятно мала (даже если производительность не имеет значения). (Но я до сих пор не понимаю, как и вы!, почему ATOM не применяется и нам вообще нужны подсказки по изоляции) - person Kristen; 18.03.2018
comment
@Kristen Заявление ATOMic, но я думаю, это потому, что за кулисами SQL блокирует запись при выполнении этой единственной операции (поскольку она все еще включает выбор, а затем обновление данных); то есть блокировки по-прежнему используются, чтобы сделать его ATOMic, но SQL автоматически позаботится об этом. Подсказка UpdLock была просто способом автора блога сказать, что SQL будет блокировать это в фоновом режиме; то есть подсказка просто сообщает SQL, что он все равно будет делать за кулисами; так что подсказка не нужна. - person JohnLBevan; 18.03.2018