Очереди SQL Server: получение и восстановление
Получайте задания атомарно, учитывайте ограничения READPAST и защищайте нового владельца от запоздавших обновлений старого обработчика.
SELECT с отдельным последующим UPDATE не обеспечивает безопасное получение задания. Два обработчика способны выбрать одну готовую строку до изменения её состояния. Очереди нужен атомарный переход с короткой фиксацией. Медленная бизнес-работа должна выполняться после завершения транзакции получения.
Атомарный выбор задания
Создайте таблицу один раз в отдельной учебной базе. Индекс помогает находить готовые задания без постоянного просмотра завершённой истории. Содержимое специально маленькое: настоящая очередь может хранить ссылку на большой документ, а не копировать его в индексы.
CREATE TABLE dbo.QueueDemo
( JobId bigint IDENTITY PRIMARY KEY, State char(1) NOT NULL,
ClaimToken uniqueidentifier NULL, LeaseUntil datetime2(3) NULL,
Payload nvarchar(100) NOT NULL );
CREATE INDEX IX_QueueDemo_Ready ON dbo.QueueDemo(State, JobId);
INSERT dbo.QueueDemo(State, Payload) VALUES ('R', N'first'), ('R', N'second');
Выполняйте получение при READ COMMITTED без внешней транзакции. Проверка явно задаёт эту границу. UPDLOCK координирует конкурентов, READPAST разрешает пропуск заблокированных строк, а READCOMMITTEDLOCK запрашивает блокировочную семантику при включённом версионировании Read Committed. Комбинация рассчитана именно на этот договор изоляции и не должна произвольно переноситься внутрь SNAPSHOT-транзакции.
IF @@TRANCOUNT <> 0 OR (@@OPTIONS & 2) = 2
THROW 50000, 'Use autocommit for this example.', 1;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
DECLARE @claimed table
(JobId bigint, ClaimToken uniqueidentifier, Payload nvarchar(100));
DECLARE @token uniqueidentifier = NEWID();
;WITH candidate AS
( SELECT TOP (1) * FROM dbo.QueueDemo
WITH (UPDLOCK, READPAST, READCOMMITTEDLOCK)
WHERE State = 'R' ORDER BY JobId )
UPDATE candidate
SET State = 'W', ClaimToken = @token,
LeaseUntil = DATEADD(minute, 5, SYSUTCDATETIME())
OUTPUT inserted.JobId, inserted.ClaimToken, inserted.Payload
INTO @claimed;
SELECT * FROM @claimed;
CTE упорядочивает кандидатов по JobId, но не гарантирует глобальный FIFO. Более раннее заблокированное задание может быть пропущено, а порядок завершения зависит от длительности работы. READPAST не обходит любые страничные блокировки и изменения схемы. Пустой результат означает только отсутствие доступного задания для этой попытки, а не отсутствие незавершённой работы вообще.
OUTPUT INTO получает идентификатор и токен из той же команды изменения. Финальный SELECT выполняется после завершения автокоммитного UPDATE. При ошибке приложение всё равно должно считать вызов неуспешным, а не запускать работу по частично полученным данным. Испытайте одновременных обработчиков и сохраните каждую пару JobId и токена.
Восстановление брошенной работы
Срок аренды определяет политику истечения, но физически не останавливает обработчик. Зависший процесс способен продолжить работу после окончания срока. Поэтому каждое изменение владельца должно заменять ClaimToken. При завершении проверяются одновременно JobId и текущий токен: старый обработчик не должен завершать попытку нового владельца.
-- Parameters supplied from the successful claim:
-- @JobId bigint, @ClaimToken uniqueidentifier
UPDATE dbo.QueueDemo
SET State = 'D', LeaseUntil = NULL
WHERE JobId = @JobId AND ClaimToken = @ClaimToken AND State = 'W';
SELECT @@ROWCOUNT AS CompletedRows;
Ноль изменённых строк означает, что состояние или владелец уже другие. Не превращайте такой результат в успешное завершение. Определите продление через heartbeat с проверкой токена, максимальную длительность и возврат просроченных заданий в готовое состояние. Восстановитель также должен обновлять строки условно и аннулировать старый токен. Слепой UPDATE после предыдущего SELECT повторяет исходную гонку.
Повторы и внешние действия
Аренда не гарантирует однократную отправку письма, платёж или удалённый вызов. Обработчик может успешно выполнить внешнее действие и упасть до сохранения результата. После истечения срока другой повторит работу. Нужен постоянный бизнес-идентификатор и идемпотентный получатель либо надёжная процедура сверки, если получатель не умеет устранять дубликаты.
Сохраняйте историю попыток и ограничивайте повторные ошибки. Постоянно неправильные данные должны переходить в ошибочное состояние с полезной диагностикой. Измеряйте возраст самого старого готового задания, просроченные аренды, повторные ошибки и задержку завершения. Одна длина очереди может скрыть единственное важное задание, которое давно не продвигается.
Перед внедрением завершите процесс после получения, задержите другой за пределы аренды и намеренно потеряйте ответ о завершении. Проверьте восстановление работы, отказ устаревшим токенам и отсутствие повторных бизнес-эффектов. Отдельно убедитесь, что пустой результат вызывает ограниченную паузу, а не бесконечный быстрый опрос базы. При продлении аренды используйте тот же токен и проверяйте число изменённых строк: старый процесс не должен продлевать чужое владение. Сохраните сценарий с восстановлением как регулярный интеграционный тест. Именно такие отказы определяют надёжность очереди точнее, чем измерение пропускной способности с безотказными обработчиками.
Техническая документация: Microsoft Learn: Table hints · Microsoft Learn: OUTPUT.