Вопрос
В чем преимущество применения блокировок к приведенному ниже утверждению?
Точно так же, какую проблему мы бы увидели, если бы не включили эти подсказки? т. е. предотвращают ли они состояние гонки, улучшают производительность или, может быть, что-то еще? Спрашиваю, возможно, они включены, чтобы предотвратить какую-то проблему, которую я не учел, а не состояние гонки, которое я предполагал.
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`
Однако, когда выбор и обновление происходят в одном и том же операторе, я бы предположил, что ни один другой сеанс не может прочитать одну и ту же запись между чтением и обновлением нашего сеанса; поэтому не было бы необходимости в явных подсказках блокировки.
Я что-то неправильно понял в том, как работают замки; или предложение для этих подсказок связано с каким-то другим подобным, но другим вариантом использования?
LCK_M_U
, но не говорится, зачем нам это нужно/в чем была бы проблема, если бы мы не беспокоились о табличных подсказках. Меня беспокоит то, что любой, кто не понимает, зачем нужны эти подсказки, может начать добавлять подсказки везде, просто на всякий случай; а использование намеков без понимания и обоснования — плохой сценарий. В исходном потоке я даже набросал код, чтобы попытаться вызвать состояние гонки; но не видел ни одного / даже не знаю, то ли это то, чего мы стремимся избежать - person JohnLBevan   schedule 18.03.2018Invoices
и имитации очереди поверх нее с подключением нескольких бизнес-систем. т. е. совет, данный в блоге, предназначен для другого сценария. - person JohnLBevan   schedule 18.03.2018