Как избежать конкурентного доступа к одной строке в таблице outbox при параллельной обработке
Что проверяют
Понимаете ли вы, что два экземпляра сервиса могут взять одну и ту же запись outbox, и знаете ли, как это исключить на уровне базы, а не мьютексом в Go. Следом почти всегда спрашивают, что будет с порядком сообщений и с дублями при падении воркера.
Как отвечать
Сначала назовите проблему: несколько подов читают строки со status = 'pending', получают одинаковые записи и оба отправляют событие в брокер. Обычная транзакция на уровне READ COMMITTED от этого не спасает, а sync.Mutex работает только внутри одного процесса.
Рабочие варианты:
FOR UPDATE SKIP LOCKED: воркер берёт пачкуSELECT id, payload FROM outbox WHERE status = 'pending' ORDER BY id LIMIT 100 FOR UPDATE SKIP LOCKED, и строки, уже занятые соседом, просто пропускаются. Есть в PostgreSQL и MySQL 8;- аренда (lease): одним
UPDATE ... SET locked_by = $1, locked_until = now() + interval '30 seconds' ... RETURNINGворкер закрепляет пачку за собой и сразу коммитит. Упавший воркер не держит строки вечно, после истечения срока их подберёт другой; - шардирование по ключу: каждый воркер обрабатывает свой остаток
hash(aggregate_id) % N. Так сохраняется порядок событий одного агрегата, чегоSKIP LOCKEDне гарантирует; - один отправитель: лидер выбирается через
pg_try_advisory_lockили lease в etcd. Просто, но упирается в пропускную способность одного процесса.
В Go это pgx или database/sql с BeginTx и контекстом с таймаутом. Обязательно проговорите, что outbox даёт at-least-once: воркер может отправить сообщение и упасть до пометки sent, поэтому потребитель должен быть идемпотентным и отбрасывать дубли по id события.
Где спотыкаются
Держат транзакцию с блокировкой открытой, пока идёт отправка в Kafka. Медленный брокер превращается в долгие блокировки и исчерпанный пул соединений.
Считают, что SELECT ... FOR UPDATE без SKIP LOCKED решает задачу. Дублей действительно не будет, но воркеры встают в очередь друг за другом, и параллельности не остаётся.
Обещают exactly-once. Без идемпотентности на стороне потребителя дубли при сбое неизбежны, и интервьюер это знает.