Какие гарантии доставки сообщений бывают? Возможна ли exactly-once и как сделать обработку идемпотентной?
Короткий ответ
- At-most-once: без повторов, возможна потеря
- At-least-once: повторы при сбоях, возможны дубли
- Exactly-once «по проводу» в общем случае недостижима
- Практика: at-least-once плюс идемпотентный потребитель
- Дедупликация по ключу сообщения в хранилище потребителя
- Outbox-паттерн связывает запись в БД и публикацию события
Реальный стандарт — at-least-once с идемпотентной обработкой: дубли неизбежны, но их эффект нейтрализуется.
Как сказать вслух
пример ответаЕсть три уровня: at-most-once — сообщение может потеряться, но не задублироваться; at-least-once — наоборот, точно дойдёт, но возможны повторы; и exactly-once, которая в распределённой системе в чистом виде недостижима — сбой между обработкой и подтверждением всегда возможен. Поэтому на практике берут at-least-once и делают обработчик идемпотентным: например, храним id уже обработанных сообщений и повторы просто пропускаем. А чтобы событие гарантированно ушло вместе с записью в базу, используют outbox-паттерн.
Подробный ответ
Основной ответ
Гарантия определяется поведением при сбое между обработкой и подтверждением. At-most-once: ack до обработки — сбой теряет сообщение. At-least-once: ack после обработки — сбой после обработки, но до ack приводит к повторной доставке. Честная exactly-once-доставка невозможна (это следствие проблемы двух генералов), но достижим exactly-once effect: обработка устроена так, что дубль не меняет результат. Приёмы: естественная идемпотентность операции (UPSERT, установка статуса), таблица обработанных message_id с уникальным ограничением в той же транзакции, что и бизнес-изменение, версионирование агрегатов. Kafka предлагает идемпотентного продюсера и транзакции для цепочек consume-process-produce внутри экосистемы Kafka, но внешние эффекты (письмо, HTTP-вызов) они не покрывают. Для согласованной публикации событий с изменением БД применяют transactional outbox: событие пишется в таблицу outbox той же транзакцией, отдельный процесс или CDC публикует его в брокер.
Ключевые моменты
- Где стоит ack. Подтверждение до обработки — риск потери, после — риск дубля. Третьего не дано, выбор определяет гарантию.
- Идемпотентный консьюмер. Уникальный индекс по message_id плюс вставка в одной транзакции с бизнес-логикой — дубль упадёт на конфликте и будет пропущен.
- Outbox / CDC. Нельзя атомарно закоммитить в БД и опубликовать в брокер — outbox сводит это к одной локальной транзакции.
- Границы Kafka transactions. Exactly-once в Kafka Streams работает внутри Kafka; отправка email или вызов стороннего API из-под неё не становятся транзакционными.
Практический контекст
Это ядро сеньорских собеседований по распределённым системам: «платёжное событие пришло дважды — что произойдёт?». Ожидается рассказ про идемпотентность на стороне потребителя с конкретной механикой (уникальный ключ, транзакция), а не вера в настройку exactly-once в брокере. Outbox-паттерн — почти обязательное упоминание, когда речь заходит о согласованности базы и шины событий.
Частые ошибки
- Утверждают, что Kafka даёт exactly-once «из коробки» для любых внешних эффектов
- Проектируют at-least-once без дедупликации, игнорируя неизбежные дубли
- Публикуют событие после коммита БД без outbox и теряют его при падении процесса