Как избежать race condition при реализации очереди

Когда несколько воркеров читают очередь из таблицы SQL Server, рано или поздно случится race condition.

Два обработчика возьмут одну и ту же запись и произойдет дублирование обработки.

Хорошая новость — это можно решить одним SQL-запросом.

===

Предположим, есть таблица, которая используется как очередь для обработки записей со следующим жизненным циклом:

* Pending - ожидает обработки * Processing - взята в обработку * Success - успешно обработана * Failed - обработка завершилась ошибкой

В SQL Server есть простой и эффективный способ это решить с помощью специальных хинтов (hint), позволяющих разработчику влиять на выполнение запроса. Для этого необходимо атомарно:

1. взять N записей со статусом Pending 2. сразу поменять статус на Processing 3. вернуть идентификаторы записей для дальнейшей работы

Пример SQL запроса:

UPDATE TOP (N) Table_Queue WITH (ROWLOCK, UPDLOCK, READPAST) SET Status = ‘Processing’ OUTPUT inserted.Id INTO @TakenIds WHERE Status = ‘Pending’ ORDER BY Id

Что делают хинты:

ROWLOCK - подсказывает SQL Server предпочесть блокировки строк, но не гарантирует их UPDLOCK - берёт update-lock сразу при чтении строк READPAST - пропускает уже заблокированные строки вместо ожидания их разблокировки

Для обеспечения быстроты выполнения запроса при росте очереди необходимо добавить индекс на таблицу:

CREATE INDEX IX_Queue_Status_Id ON Table_Queue (Status, Id)

Нюансы:

1. Даже при корректных блокировках необходимо делать обработчики идемпотентными, чтобы запись с одними и теми же данными не выполнилась дважды.

2. Может возникнуть ситуация, когда обработчик взял запись в работу, сменил статус на Processing и упал. Запись будет висеть в очереди и обработчики не смогут взять ее в работу повторно. Или произошла ошибка и статус записи поменялся на Failed. Для этих ситуаций необходимо реализовать механизм retry для таких записей:

- В таблице добавить дополнительные столбцы: ProcessingStartedAt и RetryCount. Заполнять в начале обработки записи. - В sql запрос добавить условия выборки строк для обработки:

WHERE Status IN (‘Pending’, ‘Failed’) OR ( Status = ‘Processing’ AND ProcessingStartedAt < DATEADD(minute, -5, GETDATE()) )

===

Когда использовать такой подход:

- небольшие системы - отсутствие брокера - важна простота деплоя

Когда лучше брокер:

- высокая нагрузка - сложная маршрутизация - гарантированная доставка

В результате:

✅ одна запись будет взята только одним обработчиком ✅ несколько обработчиков могут работать параллельно ✅ нет блокировок всей очереди ✅ операция выбора и смены статуса происходит атомарно