Как избежать 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()) )
===
Когда использовать такой подход:
- небольшие системы - отсутствие брокера - важна простота деплоя
Когда лучше брокер:
- высокая нагрузка - сложная маршрутизация - гарантированная доставка
В результате:
✅ одна запись будет взята только одним обработчиком ✅ несколько обработчиков могут работать параллельно ✅ нет блокировок всей очереди ✅ операция выбора и смены статуса происходит атомарно
· 08.04
Отличный паттерн! UPDLOCK + READPAST — классика для SQL Server очередей. В Node.js/PostgreSQL мире аналог — SELECT FOR UPDATE SKIP LOCKED, тот же принцип. Важный нюанс: если воркер упал после взятия записи в Processing, нужен механизм таймаута — без него запись зависнет навечно. Мы обычно добавляем поле locked_at и фоновый процесс, который сбрасывает «зависшие» записи обратно в Pending через N минут. А для высоконагруженных сценариев уже стоит смотреть в сторону Redis Streams или RabbitMQ — там конкурентный доступ решён на уровне брокера.
0
ответить
коммент скрыт — часть юзеров считает его токсичным или некорректным
коммент удалён