Как реализовать распределенную обработку задач несколькими инстансами, чтобы каждая заявка обрабатывалась только один раз
Что проверяют
Понимаете ли вы, что строго однократную доставку в распределённой системе не гарантирует никто, и умеете получить однократный эффект другими средствами. Следом спросят, что будет, если инстанс упал посреди обработки, и как не потерять заявку.
Как отвечать
Начните с формулы: однократная обработка это доставка «хотя бы один раз» плюс идемпотентный обработчик. Дальше варианты по нарастанию инфраструктуры:
- очередь в базе: таблица заявок со статусом, инстанс забирает пачку через
SELECT ... FOR UPDATE SKIP LOCKED(PostgreSQL, MySQL 8, Oracle) и помечает еёIN_PROGRESSс владельцем и временем. Зависшие по таймауту возвращаются в работу. Брокер не нужен, для умеренных объёмов этого хватает; - брокер с конкурирующими потребителями: в RabbitMQ сообщение из очереди получает один потребитель,
basicAckотправляется только после обработки, а при обрыве канала неподтверждённое сообщение вернётся в очередь. В Kafka партицию внутри consumer group читает один потребитель, смещение коммитится после обработки; - распределённая блокировка: Redisson или ShedLock для задач по расписанию, чтобы джоба запускалась на одном узле.
Идемпотентность делается на стороне результата: уникальный ключ заявки, таблица обработанных идентификаторов (inbox), запись результата в той же транзакции, что и отметка. Повторная доставка упрётся в уникальное ограничение и будет пропущена. Для публикации событий из транзакции назовите transactional outbox.
Где спотыкаются
Заявляют, что Kafka даёт exactly once. Её транзакции покрывают цепочку «прочитал из Kafka, записал в Kafka», а запись в базу или вызов внешнего API под эту гарантию не попадают.
Ставят блокировку с TTL короче времени обработки. Лок истекает, второй инстанс берёт ту же заявку, и без fencing-токена или проверки версии при записи оба доводят её до конца.
Подтверждают сообщение до обработки или в finally при любом исходе. Упавшая заявка теряется. А если подтверждение не дошло после успешной обработки, придёт дубль, и без идемпотентности он выполнится второй раз.