Вопрос проверяет понимание гарантий доставки сообщений и того, как применяются механизмы идемпотентности для достижения "практически один раз".
Существуют три уровня доставки: at-most-once (сообщение может потеряться), at-least-once (сообщение будет доставлено, но возможны дубли), и exactly-once (каждое сообщение обрабатывается ровно один раз). Настоящий exactly-once почти невозможен в распределённых системах, но его можно приблизить с помощью идемпотентности обработчика, дедупликации сообщений, транзакций в базе и атомарного подтверждения обработки. Обычно создают таблицу обработанных событий и выполняют операции в рамках одной транзакции, чтобы повторный ретрай не нарушал данные.
В системах очередей важно понимать, как часто сообщение будет обработано.
Определение: сообщение может быть доставлено 0 или 1 раз.
без подтверждений
без повторных попыток
высокая скорость
могут быть потери
Используется там, где потеря не критична.
Определение: сообщение будет доставлено хотя бы один раз, но возможны дубли.
Это стандартная модель RabbitMQ:
сообщения подтверждаются вручную
при ошибке → переотправляются
возможны дубликаты при авариях
Определение: каждое сообщение доставлено и обработано ровно один раз.
Почти недостижимо в распределённых системах из-за:
сбоев сети
повторной доставки
перезапуска воркеров
транзакционных коллизий
множественных точек отказа
Определение: идемпотентность — операция, которая при повторном выполнении не меняет результат.
Пример:
вместо balance += 100 → задаём новое значение на основе события
вместо создания записи → UPSERT
операции не должны зависеть от того, выполнялись ли они ранее
Сохраняем идентификатор сообщения:
таблица processed_messages
Redis-сет с TTL
хеш события (event id)
Алгоритм:
сообщение приходит
проверяем, существует ли ID в истории
если да → пропускаем
если нет → обрабатываем и записываем
Объединяем:
запись результата
запись processed ID
в одну транзакцию:
php
BEGIN;
if exists(processed_id = :id) then rollback; end;
update balance set amount = amount + :delta;
insert processed_id values (:id);
COMMIT;
Это делает обработку атомарной.
Проблема:
если вы ack-ните сообщение до коммита → можно потерять данные
если ack после — возможен дубликат
Решение:
ack → строго после успешного коммита
ack и commit должны быть настолько близки, насколько возможно
запись события в outbox таблицу
фиксация транзакции
воркер выгружает события из outbox и отправляет
дедупликация на стороне потребителя
Гарантирует порядок и упрощает идемпотентность.
На практике используется:
идемпотентность
хранение обработанных IDs
атомарные транзакции
ack после commit
по возможности — один поток обработки на сущность
Это даёт “практически exactly-once” — то есть гарантирует отсутствие ошибок даже при ретраях и дубликатах.
Exactly-once трудно достижим, но на практике его моделируют за счёт идемпотентности, дедупликации и транзакций. At-least-once остаётся базовым механизмом RabbitMQ, а разработчик заботится о корректности на уровне бизнес-логики.